#include "server.h"

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

char *program_name = "<not yet set>";

/*
 * Signal handler and quit flag for synchronization loop.
 * DO NOT CHANGE THIS!
 */
volatile sig_atomic_t quit = 0;

static void signal_handler(int sig) {
    quit = 1;
}

/*
 * Prints a usage message and exits.
 * @param msg Additional output message.
 */
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.
 */
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);
}

/************************************************************************
 * Task 1 - Setup the connection for the server
 *
 * Create a passive socket of domain AF_INET and type SOCK_STREAM.
 * Listen for connections on the port given by the argument `port_str'.
 * Return the file descriptor of the communication socket.
 *
 * WARNING: The communication socket is NOT the connection socket,
 *          DO NOT USE accept(2) IN THIS FUNCTION!
 *
 * @param port_str The port string.
 * @return File descriptor of the communication socket.
 *
 * Hints: getaddrinfo(3), socket(2), listen(2)
 ************************************************************************/
int 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;
}

/*******************************************************************************
 * Task 2 - Handle client connections
 *
 * Wait for a connection on the communication socket and accept it.
 *
 * Read the argument transmitted by the client from the connection and save it
 * to a buffer that can hold a C-string with up to MAX_ARGUMENT_LEN characters.
 *
 * Call execute_command() (or the respective DEMO function) with the command
 * and the argument.
 *
 * Read the result from the file pointer returned by execute_command() and send
 * it back to the client. Send the string "COMMAND_FAILED" if execute_command()
 * fails.
 *
 * @param sockfd File descriptor of the communication socket.
 * @param command Command to execute.
 *
 * Hints: accept(2), fdopen(3), fgets(3), fprintf(3), fclose(3)
 ******************************************************************************/
void server_request_handler(int sockfd, const char *command) {
    int connfd = accept(sockfd, NULL, NULL);
    if (connfd < 0)
        error_exit("accept() failed");


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

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

    FILE *fpc = NULL;
    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);
}

/*******************************************************************************
 * Task 3 - Execute command
 *
 * Create a pipe for redirection of standard output of the child process.
 *
 * Fork a child process, close stdin of the new process and redirect stdout to
 * the pipe.
 *
 * Execute the command in the variable 'command' with the single argument in
 * the variable 'argument'.
 *
 * Wait for the child process to finish. If the child process exits with
 * exit-status 0, create a FILE object for the read-end of the pipe and return
 * it. Return NULL if the child process fails.
 *
 * Do not read from the pipe in this function.
 *
 * @param command The command that will be executed.
 * @param argument The argument for the command.
 *
 * @return Address of the FILE-object for the read end of the child's standard
 *         output pipe.
 *
 * Hints: pipe(2), dup2(2), fork(2), exec(3), fdopen(3), wait(3)
 ******************************************************************************/
FILE *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) {
            } else {
                proc_output = NULL;
            }
    }
    return proc_output;
}

/*
 * Main program
 * @param argc Number of elements in argv
 * @param argv Array of command line arguments
 * @return EXIT_SUCCESS if everything is okay
 */
int main(int argc, char *argv[]) {
    // DO NOT CHANGE THE FOLLOWING DECLARATIONS
    int sockfd = -1;
    struct arguments args;  // struct for storing the parsed arguments
    program_name = argv[0];

    // Register signal handlers
    struct sigaction s;
    s.sa_handler = signal_handler;
    s.sa_flags = 0;  // no SA_RESTART!
    if (sigfillset(&s.sa_mask) < 0) {
        error_exit("sigfillset");
    }
    if (sigaction(SIGINT, &s, NULL) < 0) {
        error_exit("sigaction SIGINT");
    }
    if (sigaction(SIGTERM, &s, NULL) < 0) {
        error_exit("sigaction SIGTERM");
    }
    // register function to free resources at normal process termination
    if (atexit(free_resources) == -1) {
        error_exit("atexit failed");
    }
    parse_arguments(argc, argv, &args);

    sockfd = setup_connection(args.port_str);
    //sockfd = DEMO_setup_connection(args.port_str);

    while (!quit) {
        server_request_handler(sockfd, args.command);
        //DEMO_server_request_handler(sockfd, args.command, 0);        // internally calls execute_command()
        //DEMO_server_request_handler(sockfd, args.command, USE_DEMO); // internally calls DEMO_execute_command()
    }

    (void)fprintf(stderr, "server exiting regularly\n");
    return EXIT_SUCCESS;
}
