/*
 * adcli
 *
 * Copyright (C) 2012 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA
 *
 * Author: Stef Walter <stefw@gnome.org>
 */

#include "config.h"

#include "adcli.h"
#include "adprivate.h"
#include "tools.h"

#include <sys/stat.h>

#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <paths.h>
#include <stdio.h>
#include <unistd.h>


static char *adcli_temp_directory = NULL;
static char *adcli_krb5_conf_filename = NULL;
static char *adcli_krb5_d_directory = NULL;

enum {
	CONNECTION_LESS = 1<<0,
};

struct {
	const char *name;
	int (*function) (adcli_conn *, int, char *[]);
	const char *text;
	int flags;
} commands[] = {
	{ "info", adcli_tool_info, "Print information about a domain", CONNECTION_LESS },
	{ "join", adcli_tool_computer_join, "Join this machine to a domain", },
	{ "update", adcli_tool_computer_update, "Update machine membership in a domain", },
	{ "testjoin", adcli_tool_computer_testjoin, "Test if machine account password is valid", },
	{ "preset-computer", adcli_tool_computer_preset, "Pre setup computers accounts", },
	{ "reset-computer", adcli_tool_computer_reset, "Reset a computer account", },
	{ "delete-computer", adcli_tool_computer_delete, "Delete a computer account", },
	{ "create-user", adcli_tool_user_create, "Create a user account", },
	{ "delete-user", adcli_tool_user_delete, "Delete a user account", },
	{ "create-group", adcli_tool_group_create, "Create a group", },
	{ "delete-group", adcli_tool_group_delete, "Delete a group", },
	{ "add-member", adcli_tool_member_add, "Add users to a group", },
	{ "remove-member", adcli_tool_member_remove, "Remove users from a group", },
	{ 0, }
};

static char
short_option (int opt)
{
	if (isalpha (opt) || isdigit (opt))
		return (char)opt;
	return 0;
}

static const struct option *
find_option (const struct option *longopts,
             int opt)
{
	int i;

	for (i = 0; longopts[i].name != NULL; i++) {
		if (longopts[i].val == opt)
			return longopts + i;
	}

	return NULL;
}

void
adcli_tool_usage (const struct option *longopts,
                  const adcli_tool_desc *usages)
{
	const struct option *longopt;
	const int indent = 28;
	const char *description;
	const char *next;
	char short_name;
	int spaces;
	int len;
	int i;

	for (i = 0; usages[i].text != NULL; i++) {

		/* If no option, then this is a heading */
		if (!usages[i].option) {
			printf ("%s\n\n", usages[i].text);
			continue;
		}

		/* Only print out options we can actually parse */
		longopt = find_option (longopts, usages[i].option);
		if (!longopt)
			continue;

		short_name = short_option (usages[i].option);
		if (short_name && longopt->name)
			len = printf ("  -%c, --%s", (int)short_name, longopt->name);
		else if (longopt->name)
			len = printf ("  --%s", longopt->name);
		else
			len = printf ("  -%c", (int)short_name);
		if (longopt->has_arg)
			len += printf ("%s<%s>",
			               longopt->name ? "=" : " ",
			               usages[i].arg ? usages[i].arg : "...");
		if (len < indent) {
			spaces = indent - len;
		} else {
			printf ("\n");
			spaces = indent;
		}
		description = usages[i].text;
		while (description) {
			while (spaces-- > 0)
				fputc (' ', stdout);
			next = strchr (description, '\n');
			if (next) {
				next += 1;
				printf ("%.*s", (int)(next - description), description);
				description = next;
				spaces = indent;
			} else {
				printf ("%s\n", description);
				break;
			}
		}

	}
}

int
adcli_tool_getopt (int argc,
                   char *argv[],
                   const struct option *options)
{
	int count = 0;
	char *shorts;
	char *p;
	int ret;
	char opt;
	int i;

	/* Number of characters */
	for (i = 0; options[i].name != NULL; i++)
		count++;

	p = shorts = malloc ((count * 2) + 1);
	return_val_if_fail (shorts != NULL, -1);

	for (i = 0; i < count; i++) {
		opt = short_option (options[i].val);
		if (opt != 0) {
			*(p++) = (char)options[i].val;
			if (options[i].has_arg == required_argument)
				*(p++) = ':';
		}
	}

	*(p++) = '\0';

	ret = getopt_long (argc, argv, shorts, options, NULL);
	free (shorts);

	return ret;
}

static void
command_usage (void)
{
	int i;

	printf ("usage: adcli command <args>...\n");
	printf ("\nCommon adcli commands are:\n");
	for (i = 0; commands[i].name != NULL; i++)
		printf ("  %-15s  %s\n", commands[i].name, commands[i].text);
	printf ("\nSee 'adcli <command> --help' for more information\n");
}

char *
adcli_prompt_password_func (adcli_login_type login_type,
                            const char *name,
                            int flags,
                            void *unused_data)
{
	char *prompt;
	char *password;
	char *result;

	if (asprintf (&prompt, "Password for %s: ", name) < 0)
		return_val_if_reached (NULL);

	password = getpass (prompt);
	free (prompt);

	if (password == NULL)
		return NULL;

	result = strdup (password);
	adcli_mem_clear (password, strlen (password));

	return result;
}

char *
adcli_read_password_func (adcli_login_type login_type,
                          const char *name,
                          int flags,
                          void *unused_data)
{
	char *buffer = NULL;
	size_t length = 0;
	size_t offset = 0;
	ssize_t res;

	for (;;) {
		if (offset >= length) {
			length += 4096;
			buffer = realloc (buffer, length + 1);
			return_val_if_fail (buffer != NULL, NULL);
		}

		res = read (0, buffer + offset, length - offset);
		if (res < 0) {
			if (errno == EAGAIN || errno == EINTR)
				continue;
			warn ("couldn't read password from stdin");
			free (buffer);
			return NULL;

		} else if (res == 0) {
			buffer[offset] = '\0';
			/* remove new line character */
			if (offset > 0 && buffer[offset - 1] == '\n') {
				buffer[offset - 1] = '\0';
				if (offset > 1 && buffer[offset - 2] == '\r') {
					buffer[offset - 2] = '\0';
				}
			}
			return buffer;

		} else {
			if (memchr (buffer + offset, 0, res)) {
				warnx ("unsupported null character present in password");
				free (buffer);
				return NULL;
			}
			offset += res;
		}
	}
}

static void
cleanup_krb5_conf_directory (void)
{
	if (adcli_krb5_d_directory) {
		rmdir (adcli_krb5_d_directory);
		free (adcli_krb5_d_directory);
		adcli_krb5_d_directory = NULL;
	}

	if (adcli_krb5_conf_filename) {
		unlink (adcli_krb5_conf_filename);
		free (adcli_krb5_conf_filename);
		adcli_krb5_conf_filename = NULL;
	}

	if (adcli_temp_directory) {
		rmdir (adcli_temp_directory);
		free (adcli_temp_directory);
		adcli_temp_directory = NULL;
	}

	unsetenv ("KRB5_CONFIG");
}

static void
setup_krb5_conf_directory (adcli_conn *conn)
{
	const char *parent;
	const char *krb5_conf;
	char *filename = NULL;
	char *snippets = NULL;
	char *contents = NULL;
	char *directory = NULL;
	struct stat sb;
	int failed = 0;
	int errn = 0;
	FILE *fo;

	krb5_conf = getenv ("KRB5_CONFIG");
	if (!krb5_conf || !krb5_conf[0])
		krb5_conf = KRB5_CONFIG;

	parent = getenv ("TMPDIR");
	if (!parent || !*parent)
		parent = _PATH_TMP;

	/* Check that the config file exists, don't include if not */
	if (stat (krb5_conf, &sb) < 0) {
		if (errno != ENOENT)
			warn ("couldn't access file: %s", krb5_conf);
		krb5_conf = NULL;
	}

	if (asprintf (&directory, "%s%sadcli-krb5-XXXXXX", parent,
	              (parent[0] && parent[strlen(parent) - 1] == '/') ? "" : "/") < 0) {
		warnx ("unexpected: out of memory");
		directory = NULL; /* content is undefined */
		failed = 1;
	}

	if (!failed) {
		if (mkdtemp (directory) == NULL) {
			errn = errno;
			failed = 1;
			warnx ("couldn't create temporary directory in: %s: %s",
			       parent, strerror (errn));
		} else {
			if (asprintf (&filename, "%s/krb5.conf", directory) < 0 ||
			    asprintf (&snippets, "%s/krb5.d", directory) < 0 ||
			    asprintf (&contents, "includedir %s\n%s%s\n", snippets,
			              krb5_conf ? "include " : "",
			              krb5_conf ? krb5_conf : "") < 0) {
				warnx ("unexpected: out of memory");
				filename = NULL; /* content is undefined */
				snippets = NULL; /* content is undefined */
				contents = NULL; /* content is undefined */
				failed = 1;
			}
		}
	}

	if (!failed) {
		fo = fopen (filename, "wb");
		if (fo == NULL) {
			errn = errno;
			failed = 1;
		} else {
			fwrite (contents, 1, strlen (contents), fo);
			if (ferror (fo)) {
				errn = errno;
				failed = 1;
				fclose (fo);
			} else {
				if (fclose (fo) != 0) {
					failed = 1;
					errn = errno;
				}
			}
		}

		if (failed) {
			warnx ("couldn't write new krb5.conf file: %s: %s",
			       filename, strerror (errn));
		}
	}


	if (!failed && mkdir (snippets, 0700) < 0) {
		errn = errno;
		failed = 1;
		warnx ("couldn't write new krb5.d directory: %s: %s",
		       snippets, strerror (errn));
	}

	if (!failed) {
		adcli_conn_set_krb5_conf_dir (conn, snippets);
		adcli_temp_directory = directory;
		adcli_krb5_conf_filename = filename;
		adcli_krb5_d_directory = snippets;
		setenv ("KRB5_CONFIG", adcli_krb5_conf_filename, 1);

	} else {
		free (filename);
		free (snippets);
		free (directory);
	}

	free (contents);
	atexit (cleanup_krb5_conf_directory);
}

static void
message_func (adcli_message_type type,
              const char *message)
{
	const char *prefix = "";

	switch (type) {
	case ADCLI_MESSAGE_INFO:
		prefix = " * ";
		break;
	case ADCLI_MESSAGE_WARNING:
	case ADCLI_MESSAGE_ERROR:
		prefix = " ! ";
		break;
	}

	fprintf (stderr, "%s%s\n", prefix, message);
}

int
main (int argc,
      char *argv[])
{
	adcli_conn *conn = NULL;
	char *command = NULL;
	int skip;
	int in, out;
	int ret;
	int i;

	/*
	 * Parse the global options. We rearrange the options as
	 * necessary, in order to pass relevant options through
	 * to the commands, but also have them take effect globally.
	 */

	for (in = 1, out = 1; in < argc; in++, out++) {
		skip = 0;

		/* The non-option is the command, take it out of the arguments */
		if (argv[in][0] != '-') {
			if (!command) {
				skip = 1;
				command = argv[in];
			}

		/* The global long options */
		} else if (argv[in][1] == '-') {
			skip = 0;

			if (strcmp (argv[in], "--") == 0) {
				if (!command)
					errx (2, "no command specified");

			} else if (strcmp (argv[in], "--verbose") == 0) {
				adcli_set_message_func (message_func);

			} else if (strcmp (argv[in], "--help") == 0) {
				if (!command) {
					command_usage ();
					return 0;
				}

			} else {
				if (!command)
					errx (2, "unknown option: %s", argv[in]);
			}

		/* The global short options */
		} else {
			skip = 0;

			for (i = 1; argv[in][i] != '\0'; i++) {
				switch (argv[in][i]) {
				case 'h':
					if (!command) {
						command_usage ();
						return 0;
					}
					break;

				case 'v':
					adcli_set_message_func (message_func);
					break;

				default:
					if (!command)
						errx (2, "unknown option: -%c", (int)argv[in][i]);
					break;
				}
			}
		}

		/* Skipping this argument? */
		if (skip)
			out--;
		else
			argv[out] = argv[in];
	}

	if (command == NULL) {
		/* As a special favor if someone just typed 'adcli', help them out */
		if (argc == 1)
			command_usage ();
		else
			warnx ("no command specified");
		return 2;
	}

	argc = out;
	conn = NULL;

	/* Look for the command */
	for (i = 0; commands[i].name != NULL; i++) {
		if (strcmp (commands[i].name, command) != 0)
			continue;

		if (!(commands[i].flags & CONNECTION_LESS)) {
			conn = adcli_conn_new (NULL);
			if (conn == NULL)
				errx (-1, "unexpected memory problems");
			adcli_conn_set_password_func (conn, adcli_prompt_password_func, NULL, NULL);
			setup_krb5_conf_directory (conn);
		}

		argv[0] = command;
		ret = (commands[i].function) (conn, argc, argv);

		if (conn)
			adcli_conn_unref (conn);
		return ret;
	}

	/* At this point we have no command */
	errx (2, "'%s' is not a valid adcli command. See 'adcli --help'", command);
}
