#define _POSIX_C_SOURCE 200809L
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.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;
}

/* Read only through the newline, leaving subsequent messages for the next call. */
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) return len == 0 ? 0 : -3;
        if (c == '\0') return -3;
        if (c == '\n') {
            line[len] = '\0';
            return 1;
        }
        if (len + 1 >= size) return -2;
        line[len++] = c;
    }
}

static void
handle_client(int fd)
{
    char line[4097], reply[80];
    for (;;) {
        const char *text;
        int result = read_line(fd, line, sizeof(line));
        int quit = 0;
        if (result <= 0) {
            if (result == -1) perror("read");
            if (result == -2)
                (void)write_all(fd, "ERR line too long\n", 18);
            if (result == -3)
                (void)write_all(fd, "ERR malformed line\n", 19);
            return;
        }
        if (strncmp(line, "ECHO ", 5) == 0) {
            text = line + 5;
        } else if (strcmp(line, "TIME") == 0) {
            time_t now = time(NULL);
            struct tm tm;
            if (now == (time_t)-1 || localtime_r(&now, &tm) == NULL ||
                strftime(reply, sizeof(reply), "%Y-%m-%d %H:%M:%S %z", &tm) == 0)
                text = "ERR time unavailable";
            else
                text = reply;
        } else if (strcmp(line, "QUIT") == 0) {
            text = "Bye";
            quit = 1;
        } else {
            text = "ERR unknown command";
        }
        if (write_all(fd, text, strlen(text)) < 0 ||
            write_all(fd, "\n", 1) < 0)
            return;
        if (quit) return;
    }
}

static void
reap_children(int sig)
{
    int saved = errno;
    pid_t pid;
    (void)sig;
    do {
        pid = waitpid(-1, NULL, WNOHANG);
    } while (pid > 0 || (pid < 0 && errno == EINTR));
    errno = saved;
}

int
main(int argc, char **argv)
{
    long port = 9000;
    char *end;
    int listener, one = 1;
    struct sockaddr_in address = {0};
    struct sigaction action = {0};

    if (argc > 2) {
        fprintf(stderr, "usage: %s [port]\n", argv[0]);
        return 1;
    }
    if (argc == 2) {
        errno = 0;
        port = strtol(argv[1], &end, 10);
        if (errno || !*argv[1] || *end || port < 1 || port > 65535) {
            fprintf(stderr, "invalid port: %s\n", argv[1]);
            return 1;
        }
    }
    sigemptyset(&action.sa_mask);
    action.sa_handler = SIG_IGN;
    if (sigaction(SIGPIPE, &action, NULL) < 0) {
        perror("sigaction");
        return 1;
    }
    action.sa_handler = reap_children;
    action.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &action, NULL) < 0) {
        perror("sigaction");
        return 1;
    }
    listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener < 0) {
        perror("socket");
        return 1;
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons((unsigned short)port);
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0 ||
        bind(listener, (struct sockaddr *)&address, sizeof(address)) < 0 ||
        listen(listener, 32) < 0) {
        perror("server");
        close(listener);
        return 1;
    }
    printf("listening on port %ld\n", port);
    fflush(stdout);
    for (;;) {
        int client = accept(listener, NULL, NULL);
        pid_t pid;
        if (client < 0) {
            if (errno == EINTR || errno == ECONNABORTED) continue;
            perror("accept");
            break;
        }
        pid = fork();
        if (pid == 0) {
            close(listener);
            handle_client(client);
            close(client);
            _exit(0);
        }
        if (pid < 0) perror("fork");
        close(client);
    }
    close(listener);
    return 1;
}
