#include "cgi.h"

#include <sys/stat.h>
#include <sys/wait.h>

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

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

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

#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>

#include "header.h"
#include "response.h"
#include "utils.h"

/*
 * Upper bound of 1 MB, to be increased if we need to read more data
 */
#define MAX_BUFFER_SIZE 1 * 1024 * 1024

/*
 * Returns true/false depending on whether the provided path starts with
 * '/cgi-bin'. Checks against '/cgi-bin/' if the string path is larger than
 * the length of `/cgi-bin'.
 *
 * Also stores the main path of the request up to a ? in cgi->request_path,
 * and stores everything past ? in cgi->query_path;
 */
bool
starts_with_cgibin(const char* string)
{
	char* cgi_prefix_short = "/cgi-bin";
	size_t cgi_prefix_short_len = strlen(cgi_prefix_short);

	/* In the event a query string is passed */
	char* cgi_prefix_qry = "/cgi-bin?";
	size_t cgi_prefix_qry_len = strlen(cgi_prefix_qry);

	char* cgi_prefix_long = "/cgi-bin/";
	size_t cgi_prefix_long_len = strlen(cgi_prefix_long);

	size_t input_len = strlen(string);

	if (input_len < cgi_prefix_short_len)
	{
		return false;
	}
	if (input_len == cgi_prefix_short_len)
	{
		return strncmp(string, cgi_prefix_short, cgi_prefix_short_len) == 0;
	}

	/*
	 * In the off-chance that someone passes a string that is like
	 * '/cgi-binaajsodijaoisjd', we verify it against the longer version,
	 * as the only other acceptable string besides '/cgi-bin' is '/cgi-bin/'
	 */
	if (string[8] == '?')
	{
		return strncmp(string, cgi_prefix_qry, cgi_prefix_qry_len) == 0;
	}
	else
	{
		return strncmp(string, cgi_prefix_long, cgi_prefix_long_len) == 0;
	}
}

/*
 * Parses a REQUEST struct into a CGI struct. Ideally only called once.
 * Also separates paths from query strings. Assumes the CGI* is already
 * zero-initialized (e.g. = {0})
 *
 * Takes a REQUEST*, a CGI*, and a flag_control*.
 */
void
convert_http_to_cgi_request(REQUEST* request, CGI* cgi, struct flag_control* fc)
{
	cgi->http_request = request;

	strncpy(cgi->request_method, request->request_type == GET ? "GET" : "HEAD",
	        strlen(request->request_type == GET ? "GET" : "HEAD"));
	sprintf(cgi->server_port, "%d", fc->port);
	strcpy(cgi->server_protocol, "HTTP/1.0");
	sprintf(cgi->server_software, "%s/%.1f", SERVER_NAME, SERVER_VERSION);

	char* cgi_prefix_1 = "/cgi-bin?";
	char* cgi_prefix_2 = "/cgi-bin";

	if (strncmp(request->path, cgi_prefix_1, strlen(cgi_prefix_1)) == 0)
	{
		sprintf(cgi->path, "/");
		/* there's 9 characters in cgi_prefix_1 before the NULL byte */
		sprintf(cgi->query_string, "%s", request->path + 9);
	}
	/* Complete equality check, in-case someone requested just "/cgi-bin" */
	else if (strcmp(request->path, cgi_prefix_2) == 0)
	{
		sprintf(cgi->path, "/");
	}
	else
	{
		/*
		 * path[0] is a slash, and we want to ignore it
		 * we know this is still a valid path regardless, so there must be a
		 */
		int slash_idx = -1;
		int query_idx = -1;
		for (size_t idx = 1; idx < strlen(request->path); idx++)
		{
			if ((request->path)[idx] == '/')
			{
				slash_idx = idx;
				break;
			}
		}
		for (size_t idx = 1; idx < strlen(request->path); idx++)
		{
			if ((request->path)[idx] == '?')
			{
				query_idx = idx;
				break;
			}
		}

		if (query_idx < 0)
		{
			strncpy(cgi->path, request->path + slash_idx,
			        strlen(request->path + slash_idx));
		}
		else
		{
			strncpy(cgi->path, request->path + slash_idx,
			        query_idx - slash_idx);
			strncpy(cgi->query_string,
			        request->path + query_idx + 1, /* ignore leading ? */
			        strlen(request->path + query_idx + 1));
		}
	}

	return;
}

/*
 * Attempts to execute the CGI at the given directory. Makes a faithful
 * assumption that the .cgi file requested already exists.
 *
 * Returns the status code sent to the client.
 */
int
exec_cgi_and_respond(REQUEST* request, CGI* cgi, bool headers_only)
{
	int pipe_fd[2];
	char read_buffer[BUFSIZ] = {0};
	pid_t fork_res;

	struct stat st;

	int status;

	if (stat(cgi->parsed_path, &st) == -1)
	{
		status = 500;
		send_status_and_headers(request, status);
		fprintf(stderr, "[Debug] stat failed: %s\n", strerror(errno));
		return status;
	}
	if (cgi->http_request->if_modified_since_time != 0 &&
	    cgi->http_request->if_modified_since_time > st.st_mtime)
	{
		status = 304;
		send_status_and_headers(request, status);
		return status;
	}
	if (S_ISDIR(st.st_mode))
	{
		return send_dir_entries(request, headers_only);
	}

	if (pipe(pipe_fd) == -1)
	{
		status = 500;
		send_status_and_headers(request, status);
		fprintf(stderr, "[Debug] pipe failed: %s\n", strerror(errno));
		return status;
	}

	/* we don't want the child/cgi process to inherit our custom handler for
	 * SIGPIPE */
	restore_default_sigpipe();

	fork_res = fork();

	if (fork_res == -1)
	{
		/* restore the signal handler & close the pipes */
		ignore_sigpipe();
		(void)close(pipe_fd[0]);
		(void)close(pipe_fd[1]);

		status = 500;
		send_status_and_headers(request, status);
		fprintf(stderr, "[Debug] fork failed: %s\n", strerror(errno));
		return status;
	}

	if (fork_res == 0)
	{
		/* RFC3875 requires all params as environment variables on UNIX/POSIX */
		setenv("QUERY_STRING", cgi->query_string, 1); /* overwrite to be safe */
		setenv("REQUEST_METHOD", cgi->request_method, 1);
		setenv("SERVER_PORT", cgi->server_port, 1);
		setenv("SERVER_PROTOCOL", cgi->server_protocol, 1);
		setenv("SERVER_SOFTWARE", cgi->server_software, 1);

		(void)close(pipe_fd[0]); /* Child process, no need to read */

		if (dup2(pipe_fd[1], STDOUT_FILENO) == -1)
		{
			(void)close(pipe_fd[1]);
			_exit(EX_IOERR);
		}
		(void)close(pipe_fd[1]);

		char* cgi_argv[] = {cgi->parsed_path, NULL};
		execv(cgi->parsed_path, cgi_argv);
		_exit(
			EX_IOERR); /* Unreachable unless something catastrophic happened */
	}
	else
	{
		/* ignore the signal once again */
		ignore_sigpipe();

		(void)close(pipe_fd[1]); /* Parent process, no need to write */
		int read_bytes;

		char* buffer = (char*)calloc(MAX_BUFFER_SIZE, sizeof(char));
		if (buffer == NULL)
		{
			status = 500;
			send_status_and_headers(request, status);
			fprintf(stderr, "[Debug] calloc failed: %s\n", strerror(errno));
			return status;
		}
		size_t buffer_size = MAX_BUFFER_SIZE;
		size_t buffer_len = 0;

		/*
		 * Copying everything to a malloc'ed buffer for the following reasons:
		 * 1. .cgi's are not guaranteed to have all the headers that the sws
		 * manpage requires us to send. This way, we can parse them nicely
		 * 2. If something catastrophic happens like a 500 error, I'd rather the
		 *    client not know what happened (security concern, if anything), so
		 * I'd just default to a blank 500 error to return to the user.
		 */

		status = 200; /* faithful assumption, not always provided with cgi's */
		while (true)
		{
			read_bytes = read(pipe_fd[0], read_buffer, BUFSIZ);
			if (read_bytes < 0)
			{
				status = 500;
				send_status_and_headers(request, status);
				fprintf(stderr, "[Debug] fork failed: %s\n", strerror(errno));
				free(buffer);

				(void)close(pipe_fd[0]);
				/* In the event the child process is writing (or wants to) a
				 * lot of data to the pipe and it blocks, closing the pipe
				 * should send SIGPIPE to the process whose default behavior
				 * is to terminate it, and since we restore the default
				 * behavior of SIGPIPE for cgi's, it should be okay --
				 * unless the cgi process itself has custom logic to ignore
				 * SIGPIPE, in which case it could potentially block forever
				 * (if programmed poorly, namely). As such, we explicitly
				 * termiante the child process */
				(void)kill(fork_res, SIGKILL);
				(void)waitpid(fork_res, NULL, 0);
				return status;
			}
			if (read_bytes == 0)
			{
				break;
			}

			/*
			 * RFC 1945 requires that headers be sent as CRLF. RFC 3875,
			 * however, doesn't mention CRLF or LF endings at all. So, because
			 * of that, I'd rather play it safe and just remove carriage
			 * returns. That's what this bit does. - AJ
			 */
			int idx = 0;
			while (idx < read_bytes)
			{
				if (read_buffer[idx] == '\r' && idx + 1 < read_bytes)
				{
					/*
					 * Neat lil function I found in the SEE ALSO section when
					 * looking at memcpy's manpage. We just shuffle down the
					 * string!
					 */
					memmove(read_buffer + idx, read_buffer + idx + 1,
					        read_bytes - idx - 1);
					// read_buffer[read_bytes] = '\0';
					read_bytes -= 1;
					fprintf(stderr, "\\r found, truncated to %d bytes\n",
					        read_bytes);
				}
				else
				{
					idx++;
				}
			}

			if (buffer_len + read_bytes > buffer_size)
			{
				/*
				 * Kudos to https://stackoverflow.com/a/1986572 for illustrating
				 * best how to handle bad reallocs.
				 */
				char* new_buffer =
					realloc(buffer, buffer_size + MAX_BUFFER_SIZE);
				if (new_buffer == NULL)
				{
					status = 500;
					send_status_and_headers(request, status);
					fprintf(stderr, "[Debug] realloc failed: %s\n",
					        strerror(errno));
					free(buffer);
					(void)close(pipe_fd[0]);

					(void)kill(fork_res, SIGKILL);
					(void)waitpid(fork_res, NULL, 0);
					return status;
				}

				buffer = new_buffer;
				/*
				 * I'm a stickler for using calloc and zero-initializing my
				 * buffer, so this line is how I do that when reallocating
				 */
				memset(buffer + buffer_size, 0, MAX_BUFFER_SIZE);
				buffer_size += MAX_BUFFER_SIZE;
			}

			memcpy(buffer + buffer_len, read_buffer, read_bytes);
			buffer_len += read_bytes;

			/* Same reason here, I just really like zero-initialized buffers */
			memset(read_buffer, 0, sizeof(read_buffer));
			continue;
		}

		size_t body_start = 0;

		for (size_t idx = 0; idx < buffer_len; idx++)
		{
			fprintf(stderr, "%c", buffer[idx]);
			if (buffer[idx] == '\n' && idx + 1 < buffer_len &&
			    buffer[idx + 1] == '\n')
			{
				body_start = idx + 2;
				break;
			}

			if (strncmp(buffer + idx, "Status:", 7) == 0)
			{
				/*
				 * strtol ignores leading spaces, and stops if it finds a
				 * non-number character it shouldn't parse, so I pass in the
				 * point after "Status:"
				 */
				status = strtol(buffer + idx + 7, NULL, 10);
			}
			else if (strncmp(buffer + idx, "Content-Type:", 13) == 0)
			{
				size_t end_idx = idx + 13;
				while (end_idx < buffer_len && buffer[end_idx] != '\n')
				{
					end_idx++;
				}

				strncpy(cgi->content_type, buffer + idx + 13,
				        end_idx - (idx + 13));
			}
		}

		(void)waitpid(fork_res, NULL, 0);

		(void)close(pipe_fd[0]);

		cgi->content_length = buffer_len - body_start;

		send_status_and_headers_cgi(cgi, status);

		if (!headers_only)
		{
			int bytes_written =
				safe_write(cgi->http_request->client_fd, buffer + body_start,
			               buffer_len - body_start);

			if (bytes_written == -1)
			{
				fprintf(stderr, "[Debug] safe_write failed: %s\n",
				        strerror(errno));
			}
		}

		free(buffer);
	}

	return 200;
}