#ifdef __linux__
#define _XOPEN_SOURCE
#define _DEFAULT_SOURCE
#endif

#include "utils.h"

#include <sys/stat.h>
#ifdef __NetBSD__
#include <sys/syslimits.h>
#endif

#if defined(__sun) && defined(__illumos__)
#include "/opt/magic/include/magic.h"
#ifndef PATH_MAX
#include <limits.h>
#endif
#endif

#ifdef __linux__
#ifndef PATH_MAX
#include <linux/limits.h> /* for PATH_MAX */
#endif

const char* process_name = NULL;

const char*
getprogname()
{
	return process_name;
}
#endif

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "cgi.h"

/* Attempted to write this function in somewhat accordance with
 * https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 but kind of not
 * really due to it's complexity, just to have a baseline behavior and
 * implementation reccomendation(s). There is a bug where if the user provides
 * /~<user>/../ they will end up in the server's docroot, which while not
 * necessarily intuitive, still does not allow for any breakouts to occur (the
 * logic also naturally follows given how the parsing works, especially when
 * looking at the spec above). It also makes sense when consider the user
 * doesn't necessarily know how many terms /~<user> expands into server-side, so
 * for them it's a single term.
 * It's not a bug, it's a feature!
 */
void
try_remove_dots(char* path)
{
	if (path == NULL || path[0] != '/')
	{
		fprintf(stderr, "[Debug] try_remove_dots: path is null or does not "
		                "begin with a /. Ignoring... \n");
		return;
	}


	char tmp[PATH_MAX] = {0};
	int index = 0;

	tmp[0] = '/';
	index++;

	const char* p = path + 1;

	/* glorified pointer for components: /a/b/c/d, 1 would be b, 2 = c, etc.*/
	int entry_offset = 0;

	while (*p != '\0')
	{
		/* find next entry */
		const char* start = p;
		while (*p != '\0' && *p != '/')
		{
			p++;
		}

		int len = p - start;

		if (*p == '/')
		{
			p++;
		}

		/* for handling // */
		if (len == 0)
		{
			continue;
		}

		if (len == 1 && start[0] == '.')
		{
			continue;
		}

		if (len == 2 && start[0] == '.' && start[1] == '.')
		{
			if (entry_offset > 0)
			{
				/* attempt to "pop" last entry */
				index--; /* also to handle // */
				while (index > 0 && tmp[index - 1] != '/')
				{
					index--;
				}
				entry_offset--;
			}
			continue;
		}

		if (index > 1 && tmp[index - 1] != '/')
		{
			tmp[index] = '/';
			index++;
		}

		memcpy(tmp + index, start, len);
		index += len;
		tmp[index] = '/';
		index++;
		entry_offset++;
	}

	/* remove trailing / unless the request is literally just a / */
	if (index > 1 && tmp[index - 1] == '/')
	{
		index--;
	}

	tmp[index] = '\0';

	strncpy(path, tmp, PATH_MAX);
}

static void
sigpipe_ignore(int signo)
{
	(void)signo;
}

static void (*default_sigpipe_handler)(int) = NULL;

void
ignore_sigpipe()
{
	if ((default_sigpipe_handler = signal(SIGPIPE, sigpipe_ignore)) == SIG_ERR)
	{
		(void)fprintf(stderr,
		              "%s: Unable to ignore SIGPIPE. Cannot continue.\n",
		              getprogname());
		exit(EXIT_FAILURE);
	}
}

void
restore_default_sigpipe()
{
	if (default_sigpipe_handler == NULL)
	{
		fprintf(stderr, "[Debug] warning - restore_default_sigpipe: default "
		                "handler for SIGPIPE is NULL / hasn't been set yet!\n");
	}

	if (signal(SIGPIPE, default_sigpipe_handler) == SIG_ERR)
	{
		(void)fprintf(stderr,
		              "%s: Unable to restore SIGPIPE's default handler. Cannot "
		              "continue.\n",
		              getprogname());
		exit(EXIT_FAILURE);
	}
}

/* searches directory for index.html file */

int
search_dir(char* path)
{
	DIR* dir = opendir(path);
	if (dir == NULL)
	{
		return 0;
	}

	struct dirent* entry;

	while ((entry = readdir(dir)) != NULL)
	{
		if (strcmp(entry->d_name, "index.html") == 0)
		{
			closedir(dir);
			return true;
		}
	}

	closedir(dir);
	return false;
}

/*
 * Attempts to resolve the proper path, utilizing try_expand_home,
 * convert_http_to_cgi_request, and also get_full_main_dir_path.
 */
void
resolve_path(REQUEST* request, CGI* cgi, struct flag_control* fc, char* dir)
{
	if (strlen(request->path) && request->path[0] == '/' 
		&& request->path[1] == '~')
	{
		strncpy(request->parsed_path, request->path, strlen(request->path));
		try_expand_home(request->parsed_path);
	}
	else if (fc->c && starts_with_cgibin(request->path))
	{
		convert_http_to_cgi_request(request, cgi, fc);
		get_full_cgi_path(cgi, fc->cgi_dir, cgi->parsed_path,
		                  sizeof(request->parsed_path));
		/*
		 * headers.c uses request, so rather than rewrite tons of methods to
		 * also check for cgi conditionality, we let request also know the
		 * actual path of the cgi.
		 */
		strncpy(request->parsed_path, cgi->parsed_path,
		        strlen(cgi->parsed_path));
	}
	else
	{
		get_full_main_dir_path(request, dir, request->parsed_path,
		                       sizeof(request->parsed_path));
	}
}

/* tries expanding /~ into /home/<user>/sws/ where user is the name of the
 * user running the instance of this sws server*/
void
try_expand_home(char* path)
{
	if (path == NULL || strlen(path) < 3 || path[0] != '/' || path[1] != '~')
	{
		return;
	}

	char* start = path + 2;
	char* end = start;

	/* find out where the user ends by the presence of the next slash */
	while (*end != '\0' && *end != '/')
	{
		end++;
	}

	char user[NAME_MAX] = {0};
	const int user_len = end - start;

	/* rejects /~ and /~/ too */
	if (user_len == 0 || (user_len + 1) > (int)sizeof(user))
	{
		fprintf(stderr,
		        "%s: warning - try_expand_home(): /~ username provided is too "
		        "long. Ignoring...\n",
		        getprogname());
		return;
	}

	memcpy(user, start, user_len);
	user[user_len] = '\0';

	/* user is trying to get a directory index or so */
	char* target = "";
	if (*end == '/')
	{
		target = end + 1;
	}

	char tmp[PATH_MAX] = {0};
	int tmp_index = 0;

	int n = snprintf(tmp, PATH_MAX, "/home/%s/sws/", user);
	if (n < 0)
	{
		goto error;
	}
	tmp_index += n;

	n = snprintf(tmp + tmp_index, PATH_MAX - tmp_index, "%s", target);
	if (n < 0)
	{
		goto error;
	}
	/* tmp_index += n; */

	strncpy(path, tmp, PATH_MAX);

	return;

	/* normally unreachable */
error:
	fprintf(stderr,
	        "%s: warning - try_expand_home: snprintf: full path could not be "
	        "resolved.",
	        getprogname());
	return;
}

ssize_t
safe_write(int fd, const void* buf, size_t nbytes)
{
	const char* buffer = buf;
	ssize_t total_bytes_written = 0;

	while (total_bytes_written < (ssize_t)nbytes)
	{
		ssize_t bytesWritten = write(fd, (buffer + total_bytes_written),
		                             (nbytes - total_bytes_written));

		if (bytesWritten < 0)
		{
			if (errno == EINTR)
			{
				continue; /* try again */
			}
			return -1;
		}

		total_bytes_written += bytesWritten;
	}

	return total_bytes_written;
}

/*
 * Resolve the full path of a normal main_directory file.
 * Ignores query strings.
 */
void
get_full_main_dir_path(REQUEST* request, const char* dir_arg,
                       char* full_request_path, int full_path_bufsize)
{

	int dir_len = strlen(dir_arg);
	int req_path_len = strlen(request->path);
	int bytes_written;

	int req_path_len_no_qry = req_path_len;
	for (int i = 0; i < req_path_len; i++)
	{
		if ((request->path)[i] == '?')
		{
			req_path_len_no_qry = i;
			break;
		}
	}

	int expected_len = dir_len + req_path_len_no_qry;

	bytes_written = snprintf(full_request_path, full_path_bufsize, "%s%.*s",
	                         dir_arg, req_path_len_no_qry, request->path);

	struct stat s;
	if (stat(full_request_path, &s) < 0)
	{
		// TODO: maybe we want to send error instead?
		return;
	}

	if (search_dir(full_request_path))
	{
		snprintf(full_request_path, full_path_bufsize, "%s%s/index.html",
		         dir_arg, request->path);
	}

	if (bytes_written != expected_len)
	{
		(void)fprintf(
			stderr,
			"%s: get_full_main_dir_path: warning - full path could not be "
			"written. Expected %d bytes, wrote %d "
			"bytes\n",
			getprogname(), expected_len, bytes_written);
	}

	// printf("[Debug] get_full_main_dir_path Path: %s\n",
	// full_request_path);


	return;
}

/*
 * Resolve the full path of a CGI executable.
 */
void
get_full_cgi_path(CGI* cgi, const char* cgi_dir, char* full_request_path,
                  int full_path_bufsize)
{
	int dir_len = strlen(cgi_dir);
	int cgi_path_len = strlen(cgi->path);

	int expected_len = dir_len + cgi_path_len;

	int bytes_written = snprintf(full_request_path, full_path_bufsize, "%s%s",
	                             cgi_dir, cgi->path);

	if (bytes_written != expected_len)
	{
		(void)fprintf(stderr,
		              "%s: get_full_cgi_path: warning - full path could not be "
		              "written. Expected %d bytes, wrote %d "
		              "bytes\n",
		              getprogname(), expected_len, bytes_written);
	}

	/* printf("[Debug] get_full_main_dir_path Path: %s\n",
	 * full_request_path);
	 */

	return;
}

/* strip newline and carriage returns */
void
strip_nl_cr(char* string)
{
	if (string == NULL)
	{
		fprintf(stderr, "%s: strip_nl_cr: passed string is NULL\n",
		        getprogname());
		return;
	}

	char* p = string;
	while (*p != '\0')
	{
		if (*p == '\r' || *p == '\n')
		{
			*p = '\0';
			break;
		}
		p++;
	}
	return;
}

// NOTE: sending the status codes is implemented, need to figure out how to
// exit out if a status code is sent
void
send_status(REQUEST* request, int status)
{

	request->status = status;
	char response_line[BUFSIZ];
	char* status_message;

	switch (request->status)
	{
	case 200:
		status_message = "OK";
		break;
	case 201:
		status_message = "Created";
		break;
	case 202:
		status_message = "Accepted";
		break;
	case 204:
		status_message = "No Content";
		break;
	case 301:
		status_message = "Moved Permanently";
		break;
	case 302:
		status_message = "Moved Temporarily";
		break;
	case 304:
		status_message = "Not Modified";
		break;
	case 400:
		status_message = "Bad Request";
		break;
	case 401:
		status_message = "Unauthorized";
		break;
	case 403:
		status_message = "Forbidden";
		break;
	case 404:
		status_message = "Not Found";
		break;
	case 500:
		status_message = "Internal Server Error";
		break;
	case 501:
		status_message = "Not Implemented";
		break;
	case 502:
		status_message = "Bad Gateway";
		break;
	case 503:
		status_message = "Service Unavailable";
		break;
	default:
		status_message = "Unknown Error";
		break;
	}

	int bytes_written = snprintf(response_line, BUFSIZ, "HTTP/1.0 %d %s\r\n",
	                             request->status, status_message);

	/* not much to be done if it fails*/
	(void)safe_write(request->client_fd, response_line, bytes_written);
}

char*
format_time_GMT(void)
{
	time_t t;
	time(&t);

	struct tm* gmt_time = gmtime(&t);

	char* time_buf = (char*)malloc(BUFSIZ * sizeof(char));


	/* error check by prepending a space per the manual page */
	if (strftime(time_buf, BUFSIZ, " %A, %d %b %Y %T GMT", gmt_time) == 0)
	{
		fprintf(stderr, "%s: warning - strftime failed: %s\n", getprogname(),
		        strerror(errno));
	}
	else
	{
		/* remove leading space from the error check */
		memmove(time_buf, time_buf + 1, strlen(time_buf));
	}

	return time_buf;
}

/* Populates the REQUEST* struct with arguments */
int
parse_input(REQUEST* request, char* input)
{
	char* req_time = format_time_GMT();
	(void)strncpy(request->request_time, req_time,
		              sizeof(request->request_time));

	int count = 0;
	/* 0 = action (GET / Head )
	 * 1 = file / path
	 * 2 = HTTP Version
	 */
	char* string_parse_arr[3] = {NULL, NULL,
	                             NULL}; // 3 because we parse 3 arguments

	request->status = -1;

	/* strn as it is more portable than strl :c */
	(void)strncpy(request->request_passedin, input,
	              sizeof(request->request_passedin));

	char* pointer = strtok(input, " ");

	while (pointer != NULL && count < 3)
	{
		string_parse_arr[count] = pointer;
		pointer = strtok(NULL, " ");
		count++;
	}

	if (string_parse_arr[0] == NULL || string_parse_arr[1] == NULL ||
	    string_parse_arr[2] == NULL)
	{
		return HTTP_ERROR;
	}

	/* if the path is too long, break out early for security reasons */
	if (strlen(string_parse_arr[1]) + 1 > PATH_MAX)
	{
		return HTTP_ERROR;
	}

	(void)strncpy(request->protocol, string_parse_arr[2],
	              sizeof(request->protocol));
	(void)strncpy(request->path, string_parse_arr[1], sizeof(request->path));

	free(req_time);


	if (strcmp(string_parse_arr[0], "GET") == 0)
	{
		request->request_type = GET;
	}
	else if (strcmp(string_parse_arr[0], "HEAD") == 0)
	{
		request->request_type = HEAD;
	}
	else
	{
		return HTTP_ERROR;
	}

	/* There may be trailing newline or carriage returns in the version, we
	 * should strip them (by going over the string and replacing the first
	 * instance with nul (\0)) */
	strip_nl_cr(string_parse_arr[2]);
	if (strcmp(string_parse_arr[2], "HTTP/1.0") != 0 &&
	    strcmp(string_parse_arr[2], "HTTP/1.1") != 0)
	{
		return HTTP_ERROR;
	}

	const char* const ims_header = "If-Modified-Since:";
	const char* const ims_header_pos =
		strstr(request->request_passedin, ims_header);

	if (ims_header_pos == NULL)
	{
		return HTTP_OK;
	}

	const char* pos = ims_header_pos + strlen(ims_header);
	while (*pos == ' ')
	{
		pos++;
	}

	char time_buf[BUFSIZ] = {0};
	int i = 0;
	while (*pos != '\0' && *pos != '\r' && *pos != '\n' &&
	       i < (int)sizeof(time_buf) - 1)
	{
		time_buf[i++] = *pos++;
	}
	time_buf[i] = '\0';

	struct tm tm = {0};
	char* time_convert = strptime(time_buf, "%A, %d %b %Y %T GMT", &tm);
	if (time_convert == NULL)
	{
		/* Bad request, we will not accept for simplicity. */
		return HTTP_ERROR;
	}

	request->if_modified_since_time = timegm(&tm);

	time_t cur_time;
	time(&cur_time);

	if (request->if_modified_since_time > cur_time)
	{
		return HTTP_ERROR;
	}

	return HTTP_OK;
}

int
write_crlf_to_request(REQUEST* request)
{
	size_t len = strlen("\r\n");
	ssize_t bytesWritten = safe_write(request->client_fd, "\r\n", len);
	if (bytesWritten == -1)
	{
		return HTTP_ERROR;
	}
	return HTTP_OK;
}