/*
 * commands to manipulate shared folders
 *
 * Copyright (C) 2015 LastPass.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU 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.
 *
 * In addition, as a special exception, the copyright holders grant you
 * additional permission to link or combine this program with the OpenSSL
 * library and distribute the resulting work.  See the LICENSE.OpenSSL file
 * in this distribution for more details.
 *
 * See LICENSE.OpenSSL for more details regarding this exception.
 */
#include "cmd.h"
#include "util.h"
#include "config.h"
#include "terminal.h"
#include "agent.h"
#include "kdf.h"
#include "endpoints.h"
#include "clipboard.h"
#include "upload-queue.h"
#include "process.h"
#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>

struct share_args {
	struct session *session;
	struct blob *blob;
	enum blobsync sync;
	unsigned char key[KDF_HASH_LEN];
	const char *sharename;
	struct share *share;

	bool read_only;
	bool set_read_only;
	bool admin;
	bool set_admin;
	bool hide_passwords;
	bool set_hide_passwords;

	bool specified_limit_type;
	bool whitelist;
	bool add;
	bool remove;
	bool clear;
};

struct share_command {
	const char *name;
	const char *usage;
	int (*cmd)(struct share_command *cmd, int, char **,
		   struct share_args *share);
};

#define share_userls_usage "userls SHARE"
#define share_useradd_usage "useradd [--read-only=[true|false] --hidden=[true|false] --admin=[true|false] SHARE USERNAME"
#define share_usermod_usage "usermod [--read-only=[true|false] --hidden=[true|false] --admin=[true|false] SHARE USERNAME"
#define share_userdel_usage "userdel SHARE USERNAME"
#define share_create_usage "create SHARE"
#define share_limit_usage "limit [--deny|--allow] [--add|--rm|--clear] SHARE USERNAME [sites]"
#define share_rm_usage "rm SHARE"

static char *checkmark(int x) {
	return (x) ? "x" : "_";
}

static void die_share_usage(struct share_command *cmd)
{
	die_usage(cmd->usage);
}

static int share_userls(struct share_command *cmd, int argc, char **argv,
			struct share_args *args)
{
	UNUSED(argv);
	struct share_user *user;
	char name[40];
	LIST_HEAD(users);
	bool has_groups = false;

	if (argc)
		die_share_usage(cmd);

	if (lastpass_share_getinfo(args->session, args->share->id, &users))
		die("Unable to access user list for share %s\n",
		    args->sharename);

	terminal_printf(TERMINAL_FG_YELLOW TERMINAL_BOLD "%-40s %6s %6s %6s %6s %6s" TERMINAL_RESET "\n",
	       "User", "RO", "Admin", "Hide", "OutEnt", "Accept");

	list_for_each_entry(user, &users, list) {
		if (user->is_group) {
			has_groups = true;
			continue;
		}

		if (user->realname) {
			snprintf(name, sizeof(name), "%s <%s>",
				 user->realname, user->username);
		} else {
			snprintf(name, sizeof(name), "%s", user->username);
		}

		terminal_printf("%-40s %6s %6s %6s %6s %6s"
				"\n",
				name,
				checkmark(user->read_only),
				checkmark(user->admin),
				checkmark(user->hide_passwords),
				checkmark(user->outside_enterprise),
				checkmark(user->accepted));
	}

	if (!has_groups)
		return 0;


	terminal_printf(TERMINAL_FG_YELLOW TERMINAL_BOLD "%-40s %6s %6s %6s %6s %6s"
			TERMINAL_RESET "\n",
			"Group", "RO", "Admin", "Hide", "OutEnt", "Accept");

	list_for_each_entry(user, &users, list) {
		if (!user->is_group)
			continue;

		terminal_printf("%-40s %6s %6s %6s %6s %6s"
				"\n",
				user->username,
				checkmark(user->read_only),
				checkmark(user->admin),
				checkmark(user->hide_passwords),
				checkmark(user->outside_enterprise),
				checkmark(user->accepted));
	}
	return 0;
}

static int share_useradd(struct share_command *cmd, int argc, char **argv,
			 struct share_args *args)
{
	struct share_user new_user = {
		.read_only = args->read_only,
		.hide_passwords = args->hide_passwords,
		.admin = args->admin
	};

	if (argc != 1)
		die_share_usage(cmd);

	new_user.username = argv[0];
	lastpass_share_user_add(args->session, args->share, &new_user);
	return 0;
}

static
struct share_user *get_user_from_share(struct session *session,
				       struct share *share,
				       const char *username)
{
	struct share_user *tmp, *found = NULL;
	LIST_HEAD(users);

	if (lastpass_share_getinfo(session, share->id, &users))
		die("Unable to access user list for share %s\n", share->name);

	list_for_each_entry(tmp, &users, list) {
		if (strcmp(tmp->username, username) == 0) {
			found = tmp;
			break;
		}
	}
	if (!found)
		die("Unable to find user %s in the user list\n",
		    username);

	return found;
}


static int share_usermod(struct share_command *cmd, int argc, char **argv,
			 struct share_args *args)
{
	struct share_user *user;

	if (argc != 1)
		die_share_usage(cmd);

	user = get_user_from_share(args->session, args->share, argv[0]);

	if (args->set_read_only)
		user->read_only = args->read_only;
	if (args->set_hide_passwords)
		user->hide_passwords = args->hide_passwords;
	if (args->set_admin)
		user->admin = args->admin;

	lastpass_share_user_mod(args->session, args->share, user);
	return 0;
}

static int share_userdel(struct share_command *cmd, int argc, char **argv,
			 struct share_args *args)
{
	struct share_user *found;

	if (argc != 1)
		die_share_usage(cmd);

	found = get_user_from_share(args->session, args->share, argv[0]);
	lastpass_share_user_del(args->session, args->share->id, found);
	return 0;
}

static void print_share_limits(struct blob *blob, struct share *share,
			       struct share_limit *limit)
{
	struct account *account;
	struct share_limit_aid *aid;
	char sitename[80];

	/* display current settings for this user */
	terminal_printf(TERMINAL_FG_YELLOW TERMINAL_BOLD
			"%-60s %7s %5s" TERMINAL_RESET "\n",
			"Site", "Unavail", "Avail");

	list_for_each_entry(account, &blob->account_head, list) {
		if (account->share != share)
			continue;

		bool in_list = false;
		list_for_each_entry(aid, &limit->aid_list, list) {
			if (!strcmp(aid->aid, account->id)) {
				in_list = true;
			}
		}

		bool avail = (in_list && limit->whitelist) ||
			(!in_list && !limit->whitelist);

		snprintf(sitename, sizeof(sitename),
				TERMINAL_BOLD "%-.30s" TERMINAL_NO_BOLD " [id: %s]",
				account->name, account->id);

		terminal_printf(TERMINAL_FG_GREEN
				"%-66s" TERMINAL_RESET " %8s %5s\n",
				sitename, checkmark(!avail), checkmark(avail));

	}
}


static int share_limit(struct share_command *cmd, int argc, char **argv,
		       struct share_args *args)
{
	struct share_user *found;
	struct share_limit limit;
	struct account *account;
	struct share_limit_aid *aid, *tmp;
	struct blob *blob = args->blob;
	bool changed_list_type;
	int optind;

	struct list_head potential_set;
	struct list_head matches;

	if (argc < 1)
		die_share_usage(cmd);

	found = get_user_from_share(args->session, args->share, argv[0]);
	lastpass_share_get_limits(args->session, args->share, found, &limit);

	if (!args->specified_limit_type)
		args->whitelist = limit.whitelist;

	/*
	 * prompt if we switch list type and there are entries already, in
	 * order to avoid accidentally changing a blacklist to a whitelist
	 */
	changed_list_type = args->whitelist != limit.whitelist &&
			    !list_empty(&limit.aid_list);

	if (argc == 1 && !changed_list_type) {
		/* nothing to do, just print current limits */
		print_share_limits(blob, args->share, &limit);
		return 0;
	}

	if (changed_list_type) {
		bool isok = ask_yes_no(false,
			"Supplied limit type (%s) doesn't match existing list (%s).\nContinue and switch?",
			args->whitelist ? "default deny" : "default allow",
			limit.whitelist ? "default deny" : "default allow");

		if (!isok)
			die("Aborted.");
	}

	/* add to, or subtract from current list */
	INIT_LIST_HEAD(&potential_set);
	INIT_LIST_HEAD(&matches);

	/* search only accts in this share */
	list_for_each_entry(account, &blob->account_head, list) {
		if (account->share == args->share)
			list_add(&account->match_list, &potential_set);
	}

	for (optind = 1; optind < argc; optind++) {
		char *name = argv[optind];
		find_matching_accounts(&potential_set, name, &matches);
	}

	if (args->clear) {
		list_for_each_entry_safe(aid, tmp, &limit.aid_list, list) {
			list_del(&aid->list);
			free(aid->aid);
		}
	}

	list_for_each_entry(account, &matches, match_list) {

		/* add account to share_limit */
		bool in_list = false;
		list_for_each_entry(aid, &limit.aid_list, list) {
			if (!strcmp(aid->aid, account->id)) {
				in_list = true;
				break;
			}
		}

		if ((!in_list && args->add) || args->clear) {
			struct share_limit_aid *newaid =
				new0(struct share_limit_aid, 1);
			newaid->aid = account->id;
			list_add_tail(&newaid->list, &limit.aid_list);
		}
		else if (in_list && args->remove) {
			list_del(&aid->list);
		}
	}

	limit.whitelist = args->whitelist;

	lastpass_share_set_limits(args->session, args->share, found, &limit);

	print_share_limits(blob, args->share, &limit);

	return 0;
}

static int share_create(struct share_command *cmd, int argc, char **argv,
			struct share_args *args)
{
	if (argc != 0)
		die_share_usage(cmd);

	UNUSED(argv);

	lastpass_share_create(args->session, args->sharename);
	return 0;
}

static int share_rm(struct share_command *cmd, int argc, char **argv,
		    struct share_args *args)
{
	if (argc != 0)
		die_share_usage(cmd);

	UNUSED(argv);

	lastpass_share_delete(args->session, args->share);
	return 0;
}

#define SHARE_CMD(name) { #name, "share " share_##name##_usage, share_##name }
static struct share_command share_commands[] = {
	SHARE_CMD(userls),
	SHARE_CMD(useradd),
	SHARE_CMD(usermod),
	SHARE_CMD(userdel),
	SHARE_CMD(create),
	SHARE_CMD(rm),
	SHARE_CMD(limit),
};
#undef SHARE_CMD

/* Display more verbose usage if no subcmd is given or matched. */
static void share_help(void)
{
	terminal_fprintf(stderr, "Usage: %s %s\n", ARGV[0], cmd_share_usage);

	for (size_t i = 0; i < ARRAY_SIZE(share_commands); ++i)
		printf("  %s %s\n", ARGV[0], share_commands[i].usage);

	exit(1);
}

int cmd_share(int argc, char **argv)
{
	char *subcmd;

	static struct option long_options[] = {
		{"sync", required_argument, NULL, 'S'},
		{"color", required_argument, NULL, 'C'},
		{"read-only", required_argument, NULL, 'r'},
		{"hidden", required_argument, NULL, 'H'},
		{"admin", required_argument, NULL, 'a'},
		{"deny", no_argument, NULL, 'd'},
		{"allow", no_argument, NULL, 'w'},
		{"add", no_argument, NULL, 'A'},
		{"rm", no_argument, NULL, 'R'},
		{"clear", no_argument, NULL, 'c'},
		{0, 0, 0, 0}
	};

	struct share_args args = {
		.sync = BLOB_SYNC_AUTO,
		.read_only = true,
		.hide_passwords = true,
		.add = true,
	};

	bool invalid_params = false;
	struct share_command *command;

	/*
	 * Parse out all option commands for all subcommands, and store
	 * them in the share_args struct.
	 *
	 * All commands have at least subcmd and sharename non-option args.
	 * Additional non-option commands are passed as argc/argv to the
	 * sub-command.
	 */
	int option;
	int option_index;
	while ((option = getopt_long(argc, argv, "S:C:r:H:a:dwARc", long_options, &option_index)) != -1) {
		switch (option) {
			case 'S':
				args.sync = parse_sync_string(optarg);
				break;
			case 'C':
				terminal_set_color_mode(
					parse_color_mode_string(optarg));
				break;
			case 'r':
				args.read_only = parse_bool_arg_string(optarg);
				args.set_read_only = true;
				break;
			case 'H':
				args.hide_passwords =
					parse_bool_arg_string(optarg);
				args.set_hide_passwords = true;
				break;
			case 'a':
				args.admin = parse_bool_arg_string(optarg);
				args.set_admin = true;
				break;
			case 'w':
				args.whitelist = true;
				args.specified_limit_type = true;
				break;
			case 'd':
				args.whitelist = false;
				args.specified_limit_type = true;
				break;
			case 'A':
				args.add = true;
				args.remove = args.clear = false;
				break;
			case 'R':
				args.remove = true;
				args.add = args.clear = false;
				break;
			case 'c':
				args.clear = true;
				args.add = args.remove = false;
				break;
			case '?':
			default:
				invalid_params = true;
		}
	}

	if (argc - optind < 1)
		share_help();

	subcmd = argv[optind++];
	command = NULL;
	for (unsigned int i=0; i < ARRAY_SIZE(share_commands); i++) {
		if (strcmp(subcmd, share_commands[i].name) == 0) {
			command = &share_commands[i];
			break;
		}
	}

	if (!command)
		share_help();

	if (argc - optind < 1 || invalid_params)
		die_share_usage(command);

	args.sharename = argv[optind++];

	init_all(args.sync, args.key, &args.session, &args.blob);

	if (strcmp(subcmd, "create") != 0) {
		args.share = find_unique_share(args.blob, args.sharename);
		if (!args.share)
			die("Share %s not found.", args.sharename);
	}

	command->cmd(command, argc - optind, &argv[optind], &args);
	session_free(args.session);
	blob_free(args.blob);
	return 0;
}
