// This is a personal academic project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#include <getopt.h>
#include <arpa/inet.h>

#include <netutils.h>

#include "client.h"

#define MAX_SIZE 524
#define MAX_RECV_SIZE 1500
#define PAGE_SIZE 5
#define MAX_USERNAME_LEN 256
#define N(x) (sizeof(x)/sizeof((x)[0]))

static unsigned short get_port(const char *s) {
     char *end = 0;
     const long sl = strtol(s, &end, 10);

     if (end == s || '\0' != *end || ((LONG_MIN == sl || LONG_MAX == sl) && ERANGE == errno) || sl < 0 || sl > USHRT_MAX) {
         fprintf(stderr, "Port should be in the range of 1-65536: %s\n", s);
         exit(1);
         return 1;
     }
     return (unsigned short) sl;
}

static void version(void) {
    fprintf(stdout, "BCP Client v1.0\nITBA Protocolos de Comunicación 2022/1C -- Group 7\nSee LICENSE.md\n");
}

static void usage(const char * progname) {
    fprintf(stdout,
        "Usage: %s [OPTION]\n\n"
        "   -h                             Prints help\n"
        "   -v                             Prints client version\n"
        "   [TOKEN] -m                     Get server metrics\n"
        "   [TOKEN] -f                     Get amount of user pages\n"
        "   [TOKEN] -u <page>              Get users in a page\n"
        "   [TOKEN] -c <name>:<new_name>   Change username from <name> to <new_name>\n"
        "   [TOKEN] -p <name>:<new_pass>   Change user' with username <name> password\n"
        "   [TOKEN] -n <name>:<pass>       Adds user to proxy server\n"
        "   [TOKEN] -d <name>              Deletes user from proxy server\n"
        "   [TOKEN] -s                     Get password dissector status\n"
        "   [TOKEN] -S <state>             Modify password dissector status\n"
        "   [TOKEN] -a                     Get proxy authentication status\n"
        "   [TOKEN] -L <conf addr>         Address of management service.\n"
        "   [TOKEN] -P <conf port>         Port of management service.\n"
        "   [TOKEN] -A <state>             Modify proxy authentication status\n\n"
        "Where TOKEN is an 8-byte unsigned decimal integer.\n\n",
        progname);
    exit(EXIT_SUCCESS);
}

uint8_t parse_args(int argc, char ** argv, uint8_t * buffer, char ** address, unsigned short * port) {
    int c;

    if (argc <= 1) {
        fprintf(stderr, "You must enter a valid token\n");
        exit(EXIT_FAILURE);
    }

    bool sent_token = false;

    if (argv[1][0] != '-') {
        uint64_t token = atol(argv[1]);
        
        buffer[0] = 0x01;

        buffer[1] = token>>56;
        buffer[2] = (uint8_t) (token>>48);
        buffer[3] = (uint8_t) (token>>40);
        buffer[4] = (uint8_t) (token>>32);
        buffer[5] = (uint8_t) (token>>24);
        buffer[6] = (uint8_t) (token>>16);
        buffer[7] = (uint8_t) (token>>8);
        buffer[8] = (uint8_t) token;

        for (int i = 1; i < argc; ++i) {
            argv[i] = argv[i+1];
        }
        argc--;
        sent_token = true;
    }

    char args = 0;
    bool Lpresent = false, Ppresent = false;
    while (args < 3) {
        if (args == 1) {
            if (!(Lpresent || Ppresent))
                break;
        }
        if (args == 2) {
            if (!(Lpresent && Ppresent))
                break;
        }
        
        c = getopt(argc, argv, "hvmfu:c:p:n:d:sS:aA:L:P:");
        if (c == -1) {
            fprintf(stderr, "A valid option is required\n");
            exit(EXIT_FAILURE);
        }

        uint16_t value;
        size_t ulen, plen, nulen;
        char * p;

        switch (c) {
            case 'h':
                usage(argv[0]);
            case 'v':
                version();
                exit(EXIT_SUCCESS);
            case 'm':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_GET_METRICS;
                break;
            case 'f':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_GET_USER_PAGES;
                break;
            case 'u':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_LIST_USERS;
                value = atoi(optarg);
                buffer[10] = value >> 8;
                buffer[11] = (uint8_t) value;
                break;
            case 'c':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_MODIFY_USERNAME;
                char *n = strchr(optarg, ':');
                if (n == NULL) {
                    fprintf(stderr, "New username not found\n");
                    exit(EXIT_FAILURE);
                }
                *n = 0;
                n++;
                ulen = strlen(optarg);
                if (ulen == 0 || ulen > 255) {
                    fprintf(stderr, "Username length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                nulen = strlen(n);
                if (nulen == 0 || nulen > 255) {
                    fprintf(stderr, "New username length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                buffer[10] = (uint8_t) ulen;
                strcat((char *) buffer, optarg);
                buffer[11+ulen] = (uint8_t) nulen;
                strcat((char *) buffer, n);
                break;
            case 'p':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_MODIFY_PASSWORD;
                p = strchr(optarg, ':');
                if (p == NULL) {
                    fprintf(stderr, "New password not found\n");
                    exit(EXIT_FAILURE);
                }
                *p = 0;
                p++;
                ulen = strlen(optarg);
                if (ulen == 0 || ulen > 255) {
                    fprintf(stderr, "Username length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                plen = strlen(p);
                if (plen == 0 || plen > 255) {
                    fprintf(stderr, "Password length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                buffer[10] = (uint8_t) ulen;
                strcat((char *) buffer, optarg);
                buffer[11+ulen] = (uint8_t) plen;
                strcat((char *) buffer, p);
                break;
            case 'n':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_ADD_USER;
                p = strchr(optarg, ':');
                if (p == NULL) {
                    fprintf(stderr, "New password not found\n");
                    exit(EXIT_FAILURE);
                }
                *p = 0;
                p++;
                ulen = strlen(optarg);
                if (ulen == 0 || ulen > 255) {
                    fprintf(stderr, "Username length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                plen = strlen(p);
                if (plen == 0 || plen > 255) {
                    fprintf(stderr, "Password length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                buffer[10] = (uint8_t) ulen;
                strcat((char *) buffer, optarg);
                buffer[11+ulen] = (uint8_t) plen;
                strcat((char *) buffer, p);
                break;
            case 'd':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_DELETE_USER;
                ulen = strlen(optarg);
                if (ulen == 0 || ulen > 255) {
                    fprintf(stderr, "Username length must be between 1 and 255\n");
                    exit(EXIT_FAILURE);
                }
                buffer[10] = (uint8_t) ulen;
                strcat((char *) buffer, optarg);
                break;
            case 's':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_GET_PASSWORD_DISSECTOR_STATUS;
                break;
            case 'S':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_MODIFY_PASSWORD_DISSECTOR_STATUS;
                buffer[10] = (uint8_t) atoi(optarg);
                break;
            case 'a':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_GET_AUTHENTICATION_STATUS;
                break;
            case 'A':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                buffer[9] = CMD_MODIFY_AUTHENTICATION_STATUS;
                buffer[10] = (uint8_t) atoi(optarg);
                break;
            case 'L':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                Lpresent = true;
                *address = optarg;
                break;
            case 'P':
                if (!sent_token) {
                    fprintf(stderr, "Token required for desired command\n");
                    exit(EXIT_FAILURE);
                }
                Ppresent = true;
                *port = get_port(optarg);
                break;
            default:
                fprintf(stderr, "Unknown argument %d\n", c);
                exit(EXIT_FAILURE);
        }

        args++;
    }

    return buffer[9];
}

int parse_response(uint8_t * buffer, uint8_t cmd) {
    if (buffer[0] != 0x01) {
        fprintf(stderr, "Incorrect response version\n");
        return EXIT_FAILURE;
    }

    uint32_t now_conn = 0, total_conn = 0;
    uint64_t bytes = 0;
    uint16_t pages, k = 0, users = 0;

    switch (buffer[1]) {
        case RESPONSE_OK:
            switch (cmd) {
                case CMD_GET_METRICS:
                    fprintf(stdout, "Metrics:\n");
                    total_conn += buffer[2];
                    total_conn <<= 8;
                    total_conn += buffer[3];
                    total_conn <<= 8;
                    total_conn += buffer[4];
                    total_conn <<= 8;
                    total_conn += buffer[5];

                    now_conn += buffer[6];
                    now_conn <<= 8;
                    now_conn += buffer[7];
                    now_conn <<= 8;
                    now_conn += buffer[8];
                    now_conn <<= 8;
                    now_conn += buffer[9];

                    bytes += buffer[10];
                    bytes <<= 8;
                    bytes += buffer[11];
                    bytes <<= 8;
                    bytes += buffer[12];
                    bytes <<= 8;
                    bytes += buffer[13];
                    bytes <<= 8;
                    bytes += buffer[14];
                    bytes <<= 8;
                    bytes += buffer[15];
                    bytes <<= 8;
                    bytes += buffer[16];
                    bytes <<= 8;
                    bytes += buffer[17];

                    fprintf(stdout, "\tTotal connections: %u\n\tCurrent connections: %u\n\tTotal bytes transferred: %lu\n", total_conn, now_conn, bytes);
                    break;
                case CMD_GET_USER_PAGES:
                    pages = buffer[2];
                    pages <<= 8;
                    pages += buffer[3];
                    fprintf(stdout, "User pages: %u\n", pages);
                    break;
                case CMD_LIST_USERS:
                    fprintf(stdout, "Users:\n");
                    while (users < PAGE_SIZE && !(k > 2 && buffer[k - 1] == '\n' && buffer[k] == '\n')) {
                        if (buffer[k] == '\n')
                            users++;
                        putchar(buffer[k++]);
                    }
                    break;
                case CMD_GET_PASSWORD_DISSECTOR_STATUS:
                    fprintf(stdout, "Password dissector status: %s\n", buffer[2] ? "ON" : "OFF");
                    break;
                case CMD_GET_AUTHENTICATION_STATUS:
                    fprintf(stdout, "Proxy authentication status: %s\n", buffer[2] ? "ON" : "OFF");
                    break;
                case CMD_MODIFY_PASSWORD_DISSECTOR_STATUS:
                case CMD_MODIFY_AUTHENTICATION_STATUS:
                case CMD_MODIFY_USERNAME:
                case CMD_MODIFY_PASSWORD:
                case CMD_ADD_USER:
                case CMD_DELETE_USER:
                    fprintf(stdout, "Success\n");
                    break;
                default:
                    break;
            }
            return EXIT_SUCCESS;
        case RESPONSE_SERVER_ERROR:
            fprintf(stderr, "ERROR - Server error\n");
            break;
        case RESPONSE_INVALID_TOKEN:
            fprintf(stderr, "ERROR - Invalid token\n");
            break;
        case RESPONSE_INVALID_PARAMETER:
            fprintf(stderr, "ERROR - Parameter value is invalid for requested command\n");
            break;
        case RESPONSE_CMD_NOT_SUPPORTED:
            fprintf(stderr, "ERROR - Command not supported\n");
            break;
        case RESPONSE_VERSION_NOT_SUPPORTED:
            fprintf(stderr, "ERROR - Version not supported\n");
            break;
        case RESPONSE_UNKNOWN_COMMAND:
            fprintf(stderr, "ERROR - Unknown command\n");
            break;
        case RESPONSE_USER_NOT_FOUND:
            fprintf(stderr, "ERROR - Specified user not found\n");
            break;
        case RESPONSE_USER_ALREADY_EXISTS:
            fprintf(stderr, "ERROR - Username already exists\n");
            break;
        default:
            fprintf(stderr, "ERROR - Unknown response status\n");
            break;
    }

    return EXIT_FAILURE;
}

int main(int argc, char **argv) {
    const char *err_msg = NULL;
    uint8_t * recv_buffer = NULL;
    
    uint8_t buffer[MAX_SIZE] = {0};
    char * address = NULL;
    unsigned short port = 0;
    uint8_t cmd = parse_args(argc, argv, buffer, &address, &port);
    int client = -1;

    unsigned char buf[sizeof(struct in6_addr)];
    int domain = AF_INET;
    if (address != NULL) {
        if (inet_pton(AF_INET6, address, buf)) {
            domain = AF_INET6;
        } else if (inet_pton(AF_INET, address, buf) <= 0) { 
            err_msg = "Incorrect network address";
            goto finally;
        }
    }

    struct sockaddr_in addr_ipv4;
    if (domain == AF_INET) {
        memset(&addr_ipv4, 0, sizeof(addr_ipv4));
        addr_ipv4.sin_family = AF_INET;
        if (address != NULL) {
            if (inet_pton(AF_INET, address, &addr_ipv4.sin_addr) <= 0) {
                err_msg = "Incorrect network address";
                goto finally;
            }
        } else {
            addr_ipv4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
        }
        if (port != 0) {
            addr_ipv4.sin_port = htons(port);
        } else {
            addr_ipv4.sin_port = htons(8080);
        }
    }

    struct sockaddr_in6 addr_ipv6;
    if (domain == AF_INET6) {
        memset(&addr_ipv6, 0, sizeof(addr_ipv6));
        addr_ipv6.sin6_family = AF_INET6;
        if (inet_pton(AF_INET6, address, &addr_ipv6.sin6_addr) <= 0) {
            err_msg = "Incorrect network address";
            goto finally;
        }
        if (port != 0) {
            addr_ipv6.sin6_port = htons(port);
        } else {
            addr_ipv6.sin6_port = htons(8080);
        }
    }

    client = socket(domain, SOCK_DGRAM, IPPROTO_UDP);
    if (client < 0) {
        err_msg = "Unable to create UDP socket";
        goto finally;
    }

    char buff[SOCKADDR_TO_HUMAN_MIN + 1];
    if (domain == AF_INET) {
        sockaddr_to_human(buff, SOCKADDR_TO_HUMAN_MIN, (const struct sockaddr *) &addr_ipv4);
        fprintf(stdout, "Connecting to %s\n\n", buff);
        if (connect(client, (struct sockaddr *) &addr_ipv4, sizeof(addr_ipv4)) < 0) {
            err_msg = "Unable to connect to UDP socket";
            goto finally;
        }
    } else {
        sockaddr_to_human(buff, SOCKADDR_TO_HUMAN_MIN, (const struct sockaddr *) &addr_ipv6);
        fprintf(stdout, "Connecting to %s\n\n", buff);
        if (connect(client, (struct sockaddr *) &addr_ipv6, sizeof(addr_ipv6)) < 0) {
            err_msg = "Unable to connect to UDP socket";
            goto finally;
        }
    }

    ssize_t sent;
    if (domain == AF_INET) {
        sent = sendto(client, buffer, N(buffer), 0, NULL, sizeof(addr_ipv4));
        if (sent < 0) {
            err_msg = "Unable to send cmd";
            goto finally;
        }
    }
    else {
        sent = sendto(client, buffer, N(buffer), 0, NULL, sizeof(addr_ipv6));
        if (sent < 0) {
            err_msg = "Unable to send cmd";
            goto finally;
        }
    }
    recv_buffer = calloc(1, MAX_RECV_SIZE);
    if (recv_buffer == NULL) {
        err_msg = "Malloc failed";
        goto finally;
    }
    ssize_t received = recvfrom(client, recv_buffer, MAX_RECV_SIZE, 0, NULL, NULL);
    if (received < 0) {
        err_msg = "Unable to receive cmd";
        goto finally;
    }

    int ret = parse_response(recv_buffer, cmd);
    err_msg = NULL;
finally:
    if (recv_buffer != NULL) {
        free(recv_buffer);
    }
    if (err_msg) {
        if (errno == 0) {
            fprintf(stderr, "%s\n", err_msg);
        } else {
            perror(err_msg);
        }
        ret = EXIT_FAILURE;
    }

    if (client >= 0) {
        close(client);
    }
    return ret;
}
