#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#include "server.h"

extern char *program_name;

extern volatile sig_atomic_t quit;

/*
 * Prints a usage message and exits.
 * @param msg Additional output message.
 */
static void usage(const char *msg) {
    fprintf(stderr, "Usage: %s [-p PORT] COMMAND\n%s\n", program_name, msg);
    exit(EXIT_FAILURE);
}
/*
 * Print an error message and exit with EXIT_FAILURE.
 * @param msg Additional output message.
 */
static void error_exit(const char *msg) {
    if (errno == 0)
        fprintf(stderr, "[%s] [ERROR] %s\n", program_name, msg);
    else
        fprintf(stderr, "[%s] [ERROR] %s - %s\n", program_name, msg,
                strerror(errno));
    exit(EXIT_FAILURE);
}

static void print_error(const char *msg) {
    fprintf(stderr, "[%s] [ERROR] %s - %s\n", program_name, msg,
            strerror(errno));
}

static void print_message(const char *msg) {
    fprintf(stderr, "[%s] %s\n", program_name, msg);
}

/**
 * @brief Function freeing the resources.
 */
void free_resources(void) {
}

/*
 * Parse Arguments
 */
void parse_arguments(int argc, char *argv[], struct arguments *args) {
    memset(args, 0, sizeof(*args));

    int opt;
    while ((opt = getopt(argc, argv, "p:")) != -1) {
        switch (opt) {
            case 'p':
                if (args->port_str)
                    usage("-p option allowed only once");
                args->port_str = optarg;
                break;
            default:
                usage("invalid option");
        }
    }
    if (args->port_str == NULL)
        args->port_str = "2020";

    if (argc - optind != 1)
        usage("invalid number of arguments");

    args->command = argv[optind];
}

/*
 * Custom getaddrinfo implementation for localhost only.
 */
int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints, struct addrinfo **res) {
    struct in_addr localhost;
    localhost.s_addr = htonl(0x7f000001);  // 127.0.0.1

    // check hostname (allow only `localhost' or any string which `inet_aton()'
    // converts to 127.0.0.1)
    struct in_addr node_addr;
    if (node != NULL &&
        (inet_aton(node, &node_addr) == 0 ||
         node_addr.s_addr != localhost.s_addr) &&
        strcmp(node, "localhost") != 0)
        return EAI_NONAME;

    // check port (allow only numerical ports)
    char *endptr;
    int port = strtol(service, &endptr, 10);

    if (*endptr != '\0')
        return EAI_NONAME;

    // check hints (NULL is acceptable)
    if (hints != NULL) {
        // check family
        if (hints->ai_family != AF_UNSPEC && hints->ai_family != AF_INET)
            return EAI_FAMILY;

        // check socket type
        if (hints->ai_socktype != 0 && hints->ai_socktype != SOCK_STREAM)
            return EAI_SOCKTYPE;

        // check protocol
        if (hints->ai_protocol != 0 && hints->ai_protocol != IPPROTO_TCP)
            return EAI_SOCKTYPE;
    }

    // allocate memory for result
    *res = malloc(sizeof(struct addrinfo));
    struct sockaddr_in *sin = malloc(sizeof(struct sockaddr_in));

    if (*res == NULL || sin == NULL)
        return EAI_MEMORY;

    memset(sin, 0, sizeof(struct sockaddr_in));
    sin->sin_family = AF_INET;
    sin->sin_addr = localhost;
    sin->sin_port = htons(port);

    (*res)->ai_flags = (AI_V4MAPPED | AI_ADDRCONFIG);
    (*res)->ai_family = AF_INET;
    (*res)->ai_socktype = SOCK_STREAM;
    (*res)->ai_protocol = IPPROTO_TCP;
    (*res)->ai_addrlen = sizeof(struct sockaddr_in);
    (*res)->ai_addr = (struct sockaddr *)sin;
    (*res)->ai_canonname = NULL;
    (*res)->ai_next = NULL;

    return 0;
}

/******************************************************************************
 * DEMO FUNCTIONS
 *****************************************************************************/
int DEMO_setup_connection(const char *port_str) {
    struct addrinfo hints, *ai;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    int res = getaddrinfo(NULL, port_str, &hints, &ai);
    if (res != 0)
        error_exit("getaddrinfo() failed");

    int sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    if (sockfd < 0)
        error_exit("socket() failed");

    int reuse = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
        error_exit("setsockopt() failed");

    if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0)
        error_exit("bind() failed");

    freeaddrinfo(ai);

    if (listen(sockfd, 1) < 0)
        error_exit("listen() failed");
    return sockfd;
}

void DEMO_server_request_handler(int sockfd, const char *command, int flags) {
    int connfd = accept(sockfd, NULL, NULL);
    if (connfd < 0)
        error_exit("accept() failed");

    print_message("Accepted connection");

    FILE *fps = fdopen(connfd, "w+");

    char argument[MAX_ARGUMENT_LEN + 1] = {0};
    fgets(argument, sizeof(argument), fps);

    FILE *fpc = NULL;
    if (flags & USE_DEMO) {
        print_message("Calling DEMO_execute_command()");
        fpc = DEMO_execute_command(command, argument);
    } else {
        print_message("Calling execute_command()");
        fpc = execute_command(command, argument);
    }

    if (fpc) {
        int c;
        while ((c = fgetc(fpc)) != EOF)
            fputc(c, fps);
        fclose(fpc);
    } else {
        fputs("COMMAND_FAILED", fps);
    }
    fflush(fps);
    fclose(fps);
}

FILE *DEMO_execute_command(const char *command, const char *argument) {
    // file stream to read the output of the process
    FILE *proc_output = NULL;

    // file descriptors for the pipe for parent-child process communication
    int c2p[2];

    // create the pipes
    if (pipe(c2p) == -1)
        error_exit("cannot create pipe");

    pid_t pid = fork();
    switch (pid) {
        case 0:
            // duplicate write end to stdout
            if (dup2(c2p[1], STDOUT_FILENO) == -1) {
                error_exit("dup2() failed");
            }
            close(c2p[0]);
            close(STDIN_FILENO);

            // exec the command
            if (execlp(command, command, argument, NULL) == -1) {
                error_exit("exec() failed");
            }
        case -1:
            error_exit("fork() failed");

        default:
            // parent process
            proc_output = fdopen(c2p[0], "r");
            if (proc_output == NULL) {
                error_exit("fdopen() failed");
            }
            close(c2p[1]);

            int stat = 0;
            wait(&stat);

            if (WIFEXITED(stat) && WEXITSTATUS(stat) == 0) {
                print_message("child process exited normally");
            } else {
                print_error("child process exited abnormally");
                proc_output = NULL;
            }
    }
    return proc_output;
}
