#include "sws.h"

#include <sys/select.h>
#include <sys/socket.h>

#include <arpa/inet.h>
#include <dirent.h>
#include <errno.h>
#include <netdb.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <time.h>
#include <unistd.h>

#include "cgi.h"
#include "header.h"
#include "logging.h"
#include "response.h"
#include "structures.h"
#include "utils.h"

#define NO_CONN (-1)

static int
resolve_max_conn()
{
	int max_client_conns = sysconf(_SC_OPEN_MAX);
	if (max_client_conns == -1)
	{
		(void)fprintf(stderr,
		              "%s: warning - sysconf could not resolve process' max "
		              "number of open files. Defaulting to 1\n",
		              getprogname());
		return 1;
	}

	/* hard limited as on some OSes, such as OmniOS, ulimit -n returns 65536, 
	 * which is large enough to cause a stack overflow / stack smash here. 
	 * 1024 is the limit on NetBSD and Linux, so it should suffice as a 
	 * reasonable upper bound */
	if (max_client_conns > 1024) 
	{
		max_client_conns = 1024;
	}

	/* defensive, if by some magic open_max is set to be less than 0 and not -1
	 */
	if (max_client_conns <= 0)
	{
		max_client_conns = 1;
	}

	if (max_client_conns > 5)
	{
		/* If there's enough room for at least one connection, max_conn (the
		 * server below) should not include stdin (0), stdout (1), stderr (2),
		 * logfile (3), or the server's socket fd (4), for a total of 5. */
		max_client_conns -= 5;
	}

	/* if that number is lower than 5, various other functions will complain */

	return max_client_conns;
}

int
start_server(char* dir_arg, struct flag_control* fc)
{
	/* we need to ignore SIGPIPE in the event we send some initial/header data
	 * to a client and they immediately disconnect before we serve
	 * them the remainder of the data and such */
	ignore_sigpipe();
	/* it this fails, sws wil exit */

	int max_conn = fc->d ? 1 : resolve_max_conn();


	int server_fd;
	struct sockaddr_storage host_server_addr;
	socklen_t host_addr_size;
	int domain_type = AF_INET6;
	int ipv6_only = 0;

	if (fc->ip_addr != NULL && strchr(fc->ip_addr, ':') == NULL)
	{
		domain_type = AF_INET; /* Must be IPv4 if no colon was specified */
	}

	/* Most of the dual stack socket stuff was taken from:
	 * https://stevens.netmeister.org/631/apue-code/09/
	 * The rest (including the multiplexed IO) was taken directly from a
	 * previous CS 392 project
	 */
	server_fd = socket(domain_type, SOCK_STREAM, 0);
	if (server_fd < 0)
	{
		(void)fprintf(stderr, "%s: socket: %s\n", getprogname(),
		              strerror(errno));
		exit(EXIT_FAILURE);
	}

	if (domain_type == AF_INET6)
	{
		if (setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6_only,
		               sizeof(ipv6_only)) < 0)
		{
			(void)fprintf(stderr, "%s: setsockopt: %s\n", getprogname(),
			              strerror(errno));
			exit(EXIT_FAILURE);
		}
	}

	memset(&host_server_addr, 0, sizeof(host_server_addr));
	if (domain_type == AF_INET) /* IPv4 */
	{
		struct sockaddr_in* ipv4sock = (struct sockaddr_in*)&host_server_addr;
		ipv4sock->sin_family = AF_INET;
		ipv4sock->sin_port = htons(fc->port);
		ipv4sock->sin_addr.s_addr =
			(fc->ip_addr == NULL) ? INADDR_ANY : inet_addr(fc->ip_addr);
		host_addr_size = sizeof(*ipv4sock);

		if (inet_pton(AF_INET, fc->ip_addr, &ipv4sock->sin_addr) != 1)
		{
			(void)fprintf(stderr,
			              "%s: inet_pton (IPv4): invalid address specified\n",
			              getprogname());
			exit(EXIT_FAILURE);
		}
	}
	else
	{
		struct sockaddr_in6* ipv6sock = (struct sockaddr_in6*)&host_server_addr;
		ipv6sock->sin6_family = AF_INET6;
		ipv6sock->sin6_port = htons(fc->port);
		if (fc->ip_addr == NULL)
		{
			ipv6sock->sin6_addr =
				in6addr_any; /* all IPv6 + IPv4 (dual stack) */
		}
		else if (inet_pton(AF_INET6, fc->ip_addr, &ipv6sock->sin6_addr) != 1)
		{
			(void)fprintf(stderr,
			              "%s: inet_pton (IPv6): invalid address specified\n",
			              getprogname());
			exit(EXIT_FAILURE);
		}
		host_addr_size = sizeof(*ipv6sock);
	}


	if (bind(server_fd, (struct sockaddr*)&host_server_addr, host_addr_size) <
	    0)
	{
		(void)fprintf(stderr, "%s: bind: %s\n", getprogname(), strerror(errno));
		exit(EXIT_FAILURE);
	}

	if (listen(server_fd, max_conn) == 0)
	{
		printf("[Debug] Server listening on %s:%d\n",
		       (fc->ip_addr == NULL ? "all IP addresses on this machine"
		                            : fc->ip_addr),
		       fc->port);
	}
	else
	{
		(void)fprintf(stderr, "%s: listen: %s\n", getprogname(),
		              strerror(errno));
		exit(EXIT_FAILURE);
	}

	fd_set myset;
	int max_fd = server_fd;
	int n_conn = 0;

	int cfds[max_conn];
	struct sockaddr_storage client_addrs[max_conn];

	for (int i = 0; i < max_conn; i++)
	{
		cfds[i] = NO_CONN;
	}

	while (1)
	{
		FD_ZERO(&myset);
		FD_SET(server_fd, &myset);
		max_fd = server_fd;

		for (int i = 0; i < max_conn; i++)
		{
			if (cfds[i] != NO_CONN)
			{
				FD_SET(cfds[i], &myset);
				if (cfds[i] > max_fd)
				{
					max_fd = cfds[i];
				}
			}
		}

		if (select(max_fd + 1, &myset, NULL, NULL, NULL) < 0)
		{
			(void)fprintf(stderr, "%s: select: %s\n", getprogname(),
			              strerror(errno));
			break;
		}

		if (FD_ISSET(server_fd, &myset))
		{
			struct sockaddr_storage client_addr;
			socklen_t client_addr_size = sizeof(client_addr);

			int client_fd = accept(server_fd, (struct sockaddr*)&client_addr,
			                       &client_addr_size);
			if (client_fd < 0)
			{
				(void)fprintf(stderr, "%s: accept: %s\n", getprogname(),
				              strerror(errno));
				continue;
			}

			if (n_conn < max_conn)
			{
				n_conn++;

				(void)printf("[Debug] New connection '%d' accepted\n",
				             client_fd);

				for (int i = 0; i < max_conn; i++)
				{
					if (cfds[i] == NO_CONN)
					{
						cfds[i] = client_fd;
						client_addrs[i] = client_addr;
						break;
					}
				}
			}
			else
			{
				(void)printf(
					"[Debug] Max connection limit reached. Refusing new "
					"connection.\n");
				(void)close(client_fd);
			}
		}

		for (int i = 0; i < max_conn; i++)
		{
			if (cfds[i] != NO_CONN && FD_ISSET(cfds[i], &myset))
			{
				char buffer[PATH_MAX];
				/* May need to change the read here to be more reliable */
				int recvbytes = read(cfds[i], buffer, sizeof(buffer));
				if (recvbytes > 0)
				{
					buffer[recvbytes] = '\0';
					/* if debug is not enabled, the daemonization would have
					 * suppressed stdout */
					printf("[Debug] Received Data: %s\n", buffer);
					// found in structures.h
					REQUEST request = {0};
					request.client_fd = cfds[i];
					request.response_size =
						0; /* unnecessary assignment but for clarity */

					CGI cgi = {0};

					int response_status = 0;


					if (parse_input(&request, buffer) != HTTP_OK)
					{
						response_status = 400;
						send_status_and_headers(&request, response_status);
						goto log;
					}

					try_remove_dots(request.path);
					resolve_path(&request, &cgi, fc, dir_arg);

					if (request.request_type == HEAD)
					{
						if (fc->c && starts_with_cgibin(request.path))
						{
							response_status =
								exec_cgi_and_respond(&request, &cgi, true);
						}
						else
						{
							response_status =
								attempt_serve_main(&request, true);
						}
					}
					else if (request.request_type == GET)
					{

						if (fc->c && starts_with_cgibin(request.path))
						{
							response_status =
								exec_cgi_and_respond(&request, &cgi, false);
						}
						else
						{
							response_status =
								attempt_serve_main(&request, false);
						}
					}

				log:
					if (fc->l)
					{
						/* writes IP, Date, "request string" status
						 * RESPONSE-length, meaning it has to go after
						 * processing*/
						log_client_data(client_addrs[i], fc->log_file, fc->d,
						                &request);
					}

					(void)close(cfds[i]);
					cfds[i] = NO_CONN;
					n_conn--;
					(void)printf(
						"[Debug] Closed connection '%d': %s %s -> %d\n",
						request.client_fd,
						request.request_type == HEAD ? "HEAD" : "GET",
						request.path, response_status);
				}
				else
				{
					n_conn--;
					(void)close(cfds[i]);
					/* if debug is not enabled, the daemonization would have
					 * suppressed stdout */

					(void)printf("[Debug] Connection '%d' has been broken "
					             "(remote client lost connection)\n",
					             cfds[i]);
					cfds[i] = NO_CONN;
				}
			}
		}
	}
	/* Normally unreachable */
	(void)close(server_fd);
	return EXIT_SUCCESS;
}