#include <sys/stat.h>

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>

#include "sws.h"
#include "utils.h"

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

static const char* const opts = "dhc:i:l:p:";

static void
usage(void)
{
	/*
	 * Credit to this stackoverflow post for this neat trick to break a
	 * multiline print statement into multiple lines.
	 * https://stackoverflow.com/a/1752091
	 */
	(void)fprintf(stderr, "usage %s [options] /dir\n", getprogname());
	(void)fprintf(
		stderr,
		"options:\n"

		"-c dir\t\tAllow execution of CGIs from given directory\n"

		"-d\t\tDebugging mode (no daemonization, 1 max connection, stdout "
		"logging)\n"

		"-h\t\tPrint usage summary and exit.\n"

		"-i addr\t\tBind to given IPv4/IPv6 address. Otherwise, listen on "
		"all IPv4 & IPv6 addresses on this host.\n"

		"-l file\t\tLog requests to given file. Created if it doesn't exist\n"

		"-p port\t\tListen on given port. Default port is 8080.\n");
}

int
main(int argc, char* argv[])
{
#ifdef __linux__
	process_name = basename(argv[0]);
#endif

	extern char* optarg;
	extern int optind;
	int ch, fd;
	long int port_num;
	char* ep;

	char cgi_dir[PATH_MAX];
	char log_file[PATH_MAX];
	char main_dir[PATH_MAX];

	struct flag_control fc = {0};
	fc.c = false;
	fc.cgi_dir = cgi_dir;
	fc.d = false;
	fc.h = false;
	fc.i = false;
	fc.ip_addr = NULL;
	fc.l = false;
	fc.log_file = log_file;
	fc.p = false;
	fc.port = 8080;

	while ((ch = getopt(argc, argv, opts)) != -1)
	{
		switch (ch)
		{
		case 'd':
			fc.d = true;
			break;
		case 'h':
			usage();
			return EXIT_FAILURE; /* Most programs return 1 on -h (e.g. cp) */
		case 'c':
			fc.c = true;
			if ((fd = open(optarg, O_RDONLY | O_DIRECTORY, 0)) < 0)
			{
				(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
				              optarg, strerror(errno));
				return EX_USAGE;
			}
			(void)close(fd);
			if (realpath(optarg, fc.cgi_dir) == NULL)
			{
				(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
				              optarg, strerror(errno));
				return EX_IOERR;
			}
			break;
		case 'i':
			fc.i = true;
			/* IP address is resolved in sws.c, nothing to do here */
			fc.ip_addr = optarg;
			break;
		case 'l':
			fc.l = true;
			if ((fd = open(optarg, O_RDONLY | O_CREAT | O_APPEND,
			               S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0)
			{
				(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
				              optarg, strerror(errno));
				return EX_USAGE;
			}
			struct stat sb;
			if (fstat(fd, &sb) < 0)
			{
				(void)close(fd);
				(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
				              optarg, strerror(errno));
				return EX_IOERR;
			}
			if (!S_ISREG(sb.st_mode))
			{
				(void)close(fd);
				(void)fprintf(stderr,
				              "%s: -%c %s: Log file must be a regular file\n",
				              getprogname(), ch, optarg);
				return EX_USAGE;
			}
			(void)close(fd);
			/* fc.log_file = optarg; */
			if (realpath(optarg, fc.log_file) == NULL)
			{
				(void)fprintf(stderr, "%s: -%c %s: %s\n", getprogname(), ch,
				              optarg, strerror(errno));
				return EX_IOERR;
			}
			break;
		case 'p':
			fc.p = true;
			/* Manually set errno to check if port conversion works */
			errno = 0;
			port_num = strtol(optarg, &ep, 10);
			if (ep == optarg)
			{
				(void)fprintf(stderr, "%s: -%c %s: not a valid port.\n",
				              getprogname(), ch, optarg);
				return EX_USAGE;
			}
			if (*ep != '\0')
			{
				(void)fprintf(
					stderr, "%s: -%c %s: trailing garbage in port argument.\n",
					getprogname(), ch, optarg);
				return EX_USAGE;
			}
			if (errno && (errno == ERANGE || port_num == LONG_MAX ||
			              port_num == LONG_MIN))
			{
				(void)fprintf(stderr, "%s: -%c %s: port too small/big.\n",
				              getprogname(), ch, optarg);
				return EX_USAGE;
			}
			if (port_num < 1 || port_num > USHRT_MAX)
			{
				(void)fprintf(
					stderr,
					"%s: -%c %s: port must be between 1 and %d inclusive.\n",
					getprogname(), ch, optarg, USHRT_MAX);
				return EX_USAGE;
			}
			/* Port cannot be < 1024 if euid is not root */
			if (port_num < 1024 && geteuid() != 0)
			{
				(void)fprintf(stderr,
				              "%s: -%c %s: cannot bind to well-known port as a "
				              "non-root user.\n",
				              getprogname(), ch, optarg);
				return EX_USAGE;
			}
			fc.port = port_num;
			break;
		default:
			usage();
			return EX_USAGE;
		}
	}

	argc -= optind;
	argv += optind;

	if (argc != 1)
	{
		usage();
		return EX_USAGE;
	}

	if (realpath(argv[0], main_dir) == NULL)
	{
		(void)fprintf(stderr, "%s: %s: %s\n", getprogname(), argv[0],
		              strerror(errno));
		return EX_IOERR;
	}

	if (fc.d)
	{
		(void)printf("[Debug] Current %s PID: %d\n", getprogname(), getpid());
	}
	else
	{
		/* We do not want to have the call to daemon(3) close all the file
		 * descriptors per the assignment spec; see below
		 * nochdir to root as logging relies on knowing the process' cwd.
		 */
		if (daemon(true, true) == -1)
		{
			(void)fprintf(
				stderr,
				"%s: could not detach from the controlling terminal: %s\n",
				getprogname(), strerror(errno));
			exit(EXIT_FAILURE);
		}

		/* Redirect stdout and stdin to /dev/null (redirection and not close(2)
		 * as that's what daemon(3) claims to do) */
		int dev_null;
		if ((dev_null = open("/dev/null", O_RDWR)) == -1)
		{
			fprintf(stderr,
			        "%s: Warning: could not locate '/dev/null'. Output to "
			        "stdout will not be suppressed.\n",
			        getprogname());
			/* Early breakout */
			goto skip_stdio_redir;
		}

		/* replace stdin with /dev/null */
		if ((dup2(dev_null, STDIN_FILENO)) == -1)
		{
			fprintf(
				stderr,
				"%s: Warning: dup2 (stdin): %s. Input from stdin will *not* "
				"be ignored.\n",
				getprogname(), strerror(errno));
		}

		/* replace stdout with /dev/null */
		if ((dup2(dev_null, STDOUT_FILENO)) == -1)
		{
			fprintf(
				stderr,
				"%s: Warning: dup2 (stdout): %s. Output to stdout will not be "
				"suppressed.\n",
				getprogname(), strerror(errno));
		}
		/* assignment never said to close/suppress stderr; just ensure nothing
		 * is printed to stdout. */

		/* close /dev/null now that it has been placed in stdin and stdout's
		 * location */
		(void)close(dev_null);

	skip_stdio_redir:

		/* This will not do anything if all of the above succeeded */
		(void)printf("[Debug] New %s PID after daemonizing: %d\n",
		             getprogname(), getpid());
	}

	return start_server(main_dir, &fc);
}