/*
 * A partial implementation of HTTP/1.0
 *
 * This code is mainly intended as a replacement for the book's 'tiny.c' server
 * It provides a *partial* implementation of HTTP/1.0 which can form a basis for
 * the assignment.
 *
 * @author G. Back for CS 3214 Spring 2018
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <dirent.h>
#include <jansson.h>

#include "http.h"
#include "hexdump.h"
#include "socket.h"
#include "bufio.h"
#include "main.h"

static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret";

// Need macros here because of the sizeof
#define CRLF "\r\n"
#define CR "\r"
#define STARTS_WITH(field_name, header) \
    (!strncasecmp(field_name, header, sizeof(header) - 1))

/* Parse HTTP request line, setting req_method, req_path, and req_version. */
static bool
http_parse_request(struct http_transaction *ta)
{
    size_t req_offset;
    ssize_t len = bufio_readline(ta->client->bufio, &req_offset);
    if (len < 2)       // error, EOF, or less than 2 characters
        return false;

    char *request = bufio_offset2ptr(ta->client->bufio, req_offset);
    request[len-2] = '\0';  // replace LF with 0 to ensure zero-termination
    char *endptr;
    char *method = strtok_r(request, " ", &endptr);
    if (method == NULL)
        return false;

    if (!strcmp(method, "GET"))
        ta->req_method = HTTP_GET;
    else if (!strcmp(method, "POST"))
        ta->req_method = HTTP_POST;
    else
        ta->req_method = HTTP_UNKNOWN;

    char *req_path = strtok_r(NULL, " ", &endptr);
    if (req_path == NULL)
        return false;

    ta->req_path = bufio_ptr2offset(ta->client->bufio, req_path);

    char *http_version = strtok_r(NULL, CR, &endptr);
    if (http_version == NULL)  // would be HTTP 0.9
        return false;

    // record client's HTTP version in request
    if (!strcmp(http_version, "HTTP/1.1"))
        ta->req_version = HTTP_1_1;
    else if (!strcmp(http_version, "HTTP/1.0"))
        ta->req_version = HTTP_1_0;
    else
        return false;

    return true;
}

/* Process HTTP headers. */
static bool
http_process_headers(struct http_transaction *ta)
{
    for (;;) {
        size_t header_offset;
        ssize_t len = bufio_readline(ta->client->bufio, &header_offset);
        if (len <= 0)
            return false;

        char *header = bufio_offset2ptr(ta->client->bufio, header_offset);
        if (len == 2 && STARTS_WITH(header, CRLF))       // empty CRLF
            return true;

        header[len-2] = '\0';
        /* Each header field consists of a name followed by a 
         * colon (":") and the field value. Field names are 
         * case-insensitive. The field value MAY be preceded by 
         * any amount of LWS, though a single SP is preferred.
         */
        char *endptr;
        char *field_name = strtok_r(header, ":", &endptr);
        if (field_name == NULL)
            return false;

        // skip white space
        char *field_value = endptr;
        while (*field_value == ' ' || *field_value == '\t')
            field_value++;

        // you may print the header like so
        // printf("Header: %s: %s\n", field_name, field_value);
        if (!strcasecmp(field_name, "Content-Length")) {
            ta->req_content_len = atoi(field_value);
        }
        
        ta->valid_token = false; // special case?
        /* Handle other headers here. Both field_value and field_name
         * are zero-terminated strings.
         */
        if (!strcasecmp(field_name, "Cookie")) {
			if ((field_value == NULL) || (strlen(field_value) <= 11)
				return false;
			printf("%s\n", field_value);
			ta->token = field_value + 11; // + 11 gets rid of "auth_token=" heading.
			
			ta->valid_token = validate_token_exp(ta, ta->token);
		}
    }
}

const int MAX_HEADER_LEN = 2048;

/* add a formatted header to the response buffer. */
void 
http_add_header(buffer_t * resp, char* key, char* fmt, ...)
{
    va_list ap;

    buffer_appends(resp, key);
    buffer_appends(resp, ": ");

    va_start(ap, fmt);
    char *error = buffer_ensure_capacity(resp, MAX_HEADER_LEN);
    int len = vsnprintf(error, MAX_HEADER_LEN, fmt, ap);
    resp->len += len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len;
    va_end(ap);

    buffer_appends(resp, "\r\n");
}

/* add a content-length header. */
static void
add_content_length(buffer_t *res, size_t len)
{
    http_add_header(res, "Content-Length", "%ld", len);
}

/* start the response by writing the first line of the response 
 * to the response buffer.  Used in send_response_header */
static void
start_response(struct http_transaction * ta, buffer_t *res)
{
	switch (ta->req_version) {
	case HTTP_1_0:
		buffer_appends(res, "HTTP/1.0 ");
		break;
	case HTTP_1_1:
	default:
		buffer_appends(res, "HTTP/1.1 ");
		break;
	}

    switch (ta->resp_status) {
    case HTTP_OK:
        buffer_appends(res, "200 OK");
        break;
    case HTTP_PARTIAL_CONTENT:
        buffer_appends(res, "206 Partial Content");
        break;
    case HTTP_BAD_REQUEST:
        buffer_appends(res, "400 Bad Request");
        break;
    case HTTP_PERMISSION_DENIED:
        buffer_appends(res, "403 Permission Denied");
        break;
    case HTTP_NOT_FOUND:
        buffer_appends(res, "404 Not Found");
        break;
    case HTTP_METHOD_NOT_ALLOWED:
        buffer_appends(res, "405 Method Not Allowed");
        break;
    case HTTP_REQUEST_TIMEOUT:
        buffer_appends(res, "408 Request Timeout");
        break;
    case HTTP_REQUEST_TOO_LONG:
        buffer_appends(res, "414 Request Too Long");
        break;
    case HTTP_NOT_IMPLEMENTED:
        buffer_appends(res, "501 Not Implemented");
        break;
    case HTTP_SERVICE_UNAVAILABLE:
        buffer_appends(res, "503 Service Unavailable");
        break;
    case HTTP_INTERNAL_ERROR:
    default:
        buffer_appends(res, "500 Internal Server Error");
        break;
    }
    buffer_appends(res, CRLF);
}

/* Send response headers to client */
static bool
send_response_header(struct http_transaction *ta)
{
    buffer_t response;
    buffer_init(&response, 80);

    start_response(ta, &response);
    if (bufio_sendbuffer(ta->client->bufio, &response) == -1)
        return false;

    buffer_appends(&ta->resp_headers, CRLF);
    if (bufio_sendbuffer(ta->client->bufio, &ta->resp_headers) == -1)
        return false;

    buffer_delete(&response);
    return true;
}

/* Send a full response to client with the content in resp_body. */
static bool
send_response(struct http_transaction *ta)
{
    // add content-length.  All other headers must have already been set.
    add_content_length(&ta->resp_headers, ta->resp_body.len);

    if (!send_response_header(ta))
        return false;

    return bufio_sendbuffer(ta->client->bufio, &ta->resp_body) != -1;
}

const int MAX_ERROR_LEN = 2048;

/* Send an error response. */
static bool
send_error(struct http_transaction * ta, enum http_response_status status, const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    char *error = buffer_ensure_capacity(&ta->resp_body, MAX_ERROR_LEN);
    int len = vsnprintf(error, MAX_ERROR_LEN, fmt, ap);
    ta->resp_body.len += len > MAX_ERROR_LEN ? MAX_ERROR_LEN - 1 : len;
    va_end(ap);
    ta->resp_status = status;
    http_add_header(&ta->resp_headers, "Content-Type", "text/plain");
    return send_response(ta);
}

/* Send Not Found response. */
static bool
send_not_found(struct http_transaction *ta)
{
    return send_error(ta, HTTP_NOT_FOUND, "File %s not found", 
        bufio_offset2ptr(ta->client->bufio, ta->req_path));
}

/* A start at assigning an appropriate mime type.  Real-world 
 * servers use more extensive lists such as /etc/mime.types
 */
static const char *
guess_mime_type(char *filename)
{
    char *suffix = strrchr(filename, '.');
    if (suffix == NULL)
        return "text/plain";

    if (!strcasecmp(suffix, ".html"))
        return "text/html";

    if (!strcasecmp(suffix, ".gif"))
        return "image/gif";

    if (!strcasecmp(suffix, ".png"))
        return "image/png";

    if (!strcasecmp(suffix, ".jpg"))
        return "image/jpeg";

    if (!strcasecmp(suffix, ".js"))
        return "text/javascript";

    return "text/plain";
}

/* Check a token is not expired. */
bool validate_token_exp(struct http_transaction *ta, char* token) {
	jwt_t* cookie;
	
	// Decode token.
	int rc = jwt_decode(&cookie, ta->token, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE));
	if (rc)
		return send_error(ta, HTTP_OK, "{}");
	
	// Get claim (formatted grants).
	char* grants = jwt_get_grants_json(cookie, NULL);
	if (grants == NULL)
		return send_error(ta, HTTP_OK, "{}");
	ta->grants = grants;
	
	// Get expiration time.
	json_error_t error;
	json_t *jgrants = json_loadb(grants, strlen(grants), 0, &error);
	json_int_t exp, iat;
	const char *sub;
	json_unpack(jgrants, "{s:I, s:I, s:s}", "exp", &exp, "iat", &iat, "sub", &sub);
	
	// Check expiration time.
	if (time(NULL) >= exp)
		return false;
	
	return true;
}

/* Handle HTTP transaction for static files. */
static bool
handle_static_asset(struct http_transaction *ta, char *basedir)
{
    char fname[PATH_MAX];

    char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path);
    // The code below is vulnerable to an attack.  Can you see
    // which?  Fix it to avoid indirect object reference (IDOR) attacks.
    snprintf(fname, sizeof fname, "%s%s", basedir, req_path);
    
    char *endptr;
    char *p = calloc(strlen(fname) + 1, sizeof(char));
    memcpy(p, fname, strlen(fname) + 1);
    char *dir = strtok_r(p, "/", &endptr);
    dir = strtok_r(NULL, "/", &endptr); // initial ".." is okay
    while (dir != NULL) {
        if (!strcmp(dir, "..")) {
            return send_not_found(ta);
        }
        dir = strtok_r(NULL, "/", &endptr);
    }

    if (!strcmp(req_path, "/")) {
        memset(fname, 0, PATH_MAX);
        snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
    }


    if (access(fname, R_OK)) {
        if (errno == EACCES)
            return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
        else {
            if (html5_fallback) {
                memset(fname, 0, PATH_MAX);
                snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
            } else {
                return send_not_found(ta);
            }
        }
            
    }

    // Determine file size
    struct stat st;
    int rc = stat(fname, &st);
    if (rc == -1)
        return send_error(ta, HTTP_INTERNAL_ERROR, "Could not stat file.");

    int filefd = open(fname, O_RDONLY);
    if (filefd == -1) {
        return send_not_found(ta);
    }

    ta->resp_status = HTTP_OK;
    http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname));
    off_t from = 0, to = st.st_size - 1;

    off_t content_length = to + 1 - from;
    add_content_length(&ta->resp_headers, content_length);

    bool success = send_response_header(ta);
    if (!success)
        goto out;

    // sendfile may send fewer bytes than requested, hence the loop
    while (success && from <= to)
        success = bufio_sendfile(ta->client->bufio, filefd, &from, to + 1 - from) > 0;

out:
    close(filefd);
    return success;
}

static bool
post_handle_login(struct http_transaction *ta)
{
	char* req_body = bufio_offset2ptr(ta->client->bufio, ta->req_body);
	
	char* saveptr;
	req_body++;
	char* u_heading = strtok_r(req_body, ":", &saveptr);
	char* username = strtok_r(NULL, ",", &saveptr);
	char* p_heading = strtok_r(NULL, ":", &saveptr);
	char* password = strtok_r(NULL, "}", &saveptr);
	
	if (!strcmp(u_heading, "\"username\"") && (strlen(username) > 2)
		&& !strcmp(p_heading, "\"password\"") && (strlen(password)) > 2)
	{
		// Create cookie.
		jwt_t* cookie;
		int rc = jwt_new(&cookie);
		if (rc)
			return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_new");
		
		// Add grants.
		username++;
		username = strtok_r(username, "\"", &saveptr);
		rc = jwt_add_grant(cookie, "sub", username);
		if (rc)
			return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
		
		time_t age = time(NULL);
		rc = jwt_add_grant_int(cookie, "iat", age);
		if (rc)
			return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
		
		time_t max_age = age + 3600 * 24;
		rc = jwt_add_grant_int(cookie, "exp", max_age);
		if (rc)
			return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
		
		// Set algorithm.
		rc = jwt_set_alg(cookie, JWT_ALG_HS256, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE));
        if (rc)
			return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_set_alg");
		
		// Create token.
		char* token = jwt_encode_str(cookie);
        if (token == NULL)
			return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_encode_str");
        
        // Add Set-Cookie header.
        http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=%s; Path=%s; Max-Age=%d; HttpOnly", token, "/", max_age);
		
		// Get claim (formatted grants).
		char *grants = jwt_get_grants_json(cookie, NULL);
		buffer_appends(&ta->resp_body, grants);
		buffer_appends(&ta->resp_body, CRLF);
		
		// Send claim.
		ta->resp_status = HTTP_OK;
		return send_response(ta);
	}
	
	return false;
}


static bool get_handle_login(struct http_transaction *ta) {
    http_add_header(&ta->resp_headers, "Content-Type", "application/json");

    if (!ta->valid_token) {
        return send_error(ta, HTTP_PERMISSION_DENIED,"Permission denied.\n");
    }
    
    //~ char *json = buffer_ensure_capacity(&ta->resp_body, MAX_HEADER_LEN);
    //~ int len = snprintf(json, strlen(ta->grants) + 2, "%s\n", ta->grants);
    //~ int length = len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len;
    //~ ta->resp_body.len += length;
    
    buffer_appends(&ta->resp_body, ta->grants);
	buffer_appends(&ta->resp_body, CRLF);
    
    ta->resp_status = HTTP_OK;
    return send_response(ta);
}

/* Lists the .mp4 files in the given directory in the proper format. */
static char *list_videos(DIR* dir) {
    
    json_t *list = json_array();
    
    struct dirent *file;
    while ((file = readdir(dir)) != NULL) {
        char *suffix = strrchr(file->d_name, '.');
        if (suffix != NULL && !strcmp(suffix, ".mp4")) {
           
            // stat the file
            char fname[PATH_MAX];
            struct stat st;
            snprintf(fname, sizeof(fname), "%s/%s", server_root, file->d_name);
            int rc = stat(fname, &st);
            if (rc == -1)
                perror("Could not list videos.");
            
            // json components
            json_t *entry = json_object();
            json_t *size = json_integer(st.st_size);
            json_t *name = json_string(file->d_name);
            rc = json_object_set(entry, "size", size);
            if (rc)
                perror("JSON append failed.");
            rc = json_object_set(entry, "name", name);
            if (rc)
                perror("JSON append failed.");
            
            // add entry
            rc = json_array_append_new(list, entry);
            if (rc)
                perror("JSON append failed.");
        }
    }
    
    return json_dumps(list, 0);
}


static int val_api_url(struct http_transaction *ta) {
    char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path);
    if (!strcmp(req_path, "/api/login")) {
        return 0;
    }
    if (!strcmp(req_path, "/api/video")) {
        return 1;
    }
    if (!strcmp(req_path, "/api/logout")) {
        return 2;
    }
    return -1;

}
static bool
handle_api(struct http_transaction *ta)
{
    int val = val_api_url(ta);
    
    if (val == -1){
        return send_not_found(ta);
    }
   
    if (val == 0) {
        if (ta->req_method == HTTP_POST) {
            // Handle login post
            return post_handle_login(ta);
        }
            
        else if (ta->req_method == HTTP_GET){
            // Handle login get
            return get_handle_login(ta);
        }
            
        else{
            return send_error(ta, HTTP_METHOD_NOT_ALLOWED, "Unknown method.\n");
        }
            
    }
    if (val == 1) {
		http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes");
        DIR* dir = opendir(server_root);
        char *json = list_videos(dir);
        fprintf(stderr, "json: %s\n", json);
        
        http_add_header(&ta->resp_headers, "Content-Type", "application/json");
        
        
        char *body = buffer_ensure_capacity(&ta->resp_body, MAX_HEADER_LEN);
        int len = snprintf(body, strlen(json) + 2, "%s\n", json);
        int length = len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len;
        ta->resp_body.len += length;
        ta->resp_status = HTTP_OK;
        
        return send_response(ta);
    }
    if (ta->req_method == HTTP_POST) // handles logout
    {
		 // Add Set-Cookie header.
        http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=; Path=/; Max-Age=0; HttpOnly");
        
		// Send message.
		buffer_appends(&ta->resp_body, "{\"message\":\"logging out\"}");
		buffer_appends(&ta->resp_body, CRLF);
		ta->resp_status = HTTP_OK;
		return send_response(ta);
	}
    
    return false;

}

/* Set up an http client, associating it with a bufio buffer. */
void 
http_setup_client(struct http_client *self, struct bufio *bufio)
{
    self->bufio = bufio;
}

static bool
handle_private(struct http_transaction *ta)
{
	if (ta->valid_token) {
        return handle_static_asset(ta, server_root);
    }
    return send_error(ta, HTTP_PERMISSION_DENIED,"Forbidden.\n");
}

/* Handle a single HTTP transaction.  Returns true on success. */
bool
http_handle_transaction(struct http_client *self)
{
    struct http_transaction ta;
    memset(&ta, 0, sizeof ta);
    ta.client = self;

    if (!http_parse_request(&ta))
        return false;
    
    bool is_http_1_1 = false; // false
    if (ta.req_version == HTTP_1_1)
		is_http_1_1 = true; // true

    if (!http_process_headers(&ta))
        return false;

    if (ta.req_content_len > 0) {
        int rc = bufio_read(self->bufio, ta.req_content_len, &ta.req_body);
        if (rc != ta.req_content_len)
            return false;

        // To see the body, use this:
        // char *body = bufio_offset2ptr(ta.client->bufio, ta.req_body);
        // hexdump(body, ta.req_content_len);
    }

    buffer_init(&ta.resp_headers, 1024);
    http_add_header(&ta.resp_headers, "Server", "CS3214-Personal-Server");
    buffer_init(&ta.resp_body, 0);

    bool rc = false;
    char *req_path = bufio_offset2ptr(ta.client->bufio, ta.req_path);
    if (STARTS_WITH(req_path, "/api")) {
        rc = handle_api(&ta);
    } else
    if (STARTS_WITH(req_path, "/private")) {
        rc = handle_private(&ta);
    } else {
        rc = handle_static_asset(&ta, server_root);
    }

    buffer_delete(&ta.resp_headers);
    buffer_delete(&ta.resp_body);

    return is_http_1_1 && rc;
}
