/*
 * Copyright (C) 2015-2026 Tobias Brunner
 * Copyright (C) 2009 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#include "command.h"

#define _GNU_SOURCE
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#include <library.h>
#include <utils/debug.h>
#include <utils/optionsfrom.h>

/**
 * Registered commands.
 */
static command_t cmds[MAX_COMMANDS];

/**
 * active command.
 */
static int active = 0;

/**
 * number of registered commands
 */
static int registered = 0;

/**
 * help command index
 */
static int help_idx;

/**
 * Uri to connect to
 */
static char *uri = NULL;

static int argc;

static char **argv;

static options_t *options;

/**
 * Global options used by all subcommands
 */
static struct option command_opts[(MAX_COMMANDS > MAX_OPTIONS ?
									MAX_COMMANDS : MAX_OPTIONS) + 1];

/**
 * Global optstring used by all subcommands
 */
static char command_optstring[(MAX_COMMANDS > MAX_OPTIONS ?
								MAX_COMMANDS : MAX_OPTIONS) * 3];

/**
 * Common options
 */
static command_option_t shared_options[] = {
	{
		"debug", 'v', 1, "set debug level, default: 1"
	},
	{
		"options", '+', 1, "read command line options from file"
	},
	{
		"uri", 'u', 1, "service URI to connect to"
	},
};

/**
 * Add a single option to command_opts/command_optstr at the current locations.
 */
static void build_option(command_option_t *option, int i, int *pos)
{
	command_opts[i].name = option->name;
	command_opts[i].has_arg = option->arg;
	command_opts[i].val = option->op;

	command_optstring[(*pos)++] = option->op;
	switch (option->arg)
	{
		case optional_argument:
			command_optstring[(*pos)++] = ':';
			/* FALL */
		case required_argument:
			command_optstring[(*pos)++] = ':';
			/* FALL */
		case no_argument:
		default:
			break;
	}
}

/**
 * Build command_opts/command_optstr for the active command
 */
static void build_opts()
{
	int i, j, pos = 0;

	memset(command_opts, 0, sizeof(command_opts));
	memset(command_optstring, 0, sizeof(command_optstring));
	if (active == help_idx)
	{
		for (i = 0; i < MAX_COMMANDS && cmds[i].cmd; i++)
		{
			command_opts[i].name = cmds[i].cmd;
			command_opts[i].val = cmds[i].op;
			command_optstring[i] = cmds[i].op;
		}
		/* add shared options not as actual option but as commands with
		 * argument (which commands usually don't take) */
		if (i > MAX_COMMANDS - countof(shared_options))
		{
			fprintf(stderr, "unable to add global shared options, please "
					"increase MAX_COMMANDS\n");
		}
		else
		{
			for (j = 0, pos = i; j < countof(shared_options); j++, i++)
			{
				build_option(&shared_options[j], i, &pos);
			}
		}
	}
	else
	{
		for (i = 0; cmds[active].options[i].name; i++)
		{
			build_option(&cmds[active].options[i], i, &pos);
		}
	}
}

/**
 * getopt_long wrapper
 */
int command_getopt(char **arg)
{
	int op;

	while (TRUE)
	{
		op = getopt_long(argc, argv, command_optstring, command_opts, NULL);
		switch (op)
		{
			case '+':
			case 'v':
			case 'u':
				continue;
			default:
				if (arg)
				{
					*arg = optarg;
				}
				return op;
		}
	}
}

/**
 * Pre-process options common for all commands
 */
static bool process_common_opts(bool init)
{
	int prevoptind = optind;
	bool success = TRUE;

	/* don't report any errors during this pre-processing */
	opterr = 0;
	while (TRUE)
	{
		switch (getopt_long(argc, argv, command_optstring, command_opts, NULL))
		{
			case '+':
				if (!options->from(options, optarg, &argc, &argv, optind))
				{
					success = FALSE;
					break;
				}
				continue;
			case 'v':
				dbg_default_set_level(atoi(optarg));
				continue;
			case 'u':
				uri = optarg;
				continue;
			default:
				if (init)
				{	/* stop if we found a known command during initialization,
					 * otherwise we'd process e.g. --options twice */
					break;
				}
				continue;
			case '?':
			case EOF:
				break;
		}
		break;
	}
	/* restore option parser state after pre-processing */
	optind = prevoptind;
	opterr = 1;
	return success;
}

/**
 * Register a command
 */
void command_register(command_t command)
{
	int i, j;

	if (registered == MAX_COMMANDS)
	{
		fprintf(stderr, "unable to register command, please increase "
				"MAX_COMMANDS\n");
		return;
	}
	for (i = 0; i < MAX_COMMANDS && cmds[i].cmd; i++)
	{
		if (cmds[i].op == command.op)
		{
			fprintf(stderr, "unable to register command --%s, short option "
					"conflicts with --%s\n", command.cmd, cmds[i].cmd);
			return;
		}
	}
	for (i = 0; i < countof(shared_options); i++)
	{
		if (shared_options[i].op == command.op)
		{
			fprintf(stderr, "unable to register command --%s, short option "
					"-%c conflicts with --%s\n", command.cmd, command.op,
					shared_options[i].name);
			return;
		}
	}

	cmds[registered] = command;
	/* append default options, but not to --help */
	if (!active)
	{
		for (i = 0; i < MAX_OPTIONS; i++)
		{
			if (!cmds[registered].options[i].name)
			{
				break;
			}
		}
		if (i > MAX_OPTIONS - countof(shared_options))
		{
			fprintf(stderr, "command '%s' registered too many options, please "
					"increase MAX_OPTIONS\n", command.cmd);
		}
		else
		{
			for (j = 0; j < countof(shared_options); j++)
			{
				cmds[registered].options[i++] = shared_options[j];
			}
		}
		for (i = 0; cmds[registered].line[i]; i++)
		{
			if (i == MAX_LINES - 1)
			{
				fprintf(stderr, "command '%s' specifies too many usage summary "
						"lines, please increase MAX_LINES\n", command.cmd);
				break;
			}
		}
	}
	registered++;
}

/**
 * Print usage text, with an optional error
 */
int command_usage(char *error, ...)
{
	va_list args;
	FILE *out = stdout;
	int i;

	if (error)
	{
		out = stderr;
	}
	fprintf(out, "strongSwan %s swanctl", VERSION);

	if (active == help_idx)
	{
		fprintf(out, "\n");
		if (lib)
		{
			fprintf(out, "loaded plugins: %s\nusage:\n"
					"  swanctl command [options]\ncommands:\n",
					lib->plugins->loaded_plugins(lib->plugins));
		}
		for (i = 0; i < MAX_COMMANDS && cmds[i].cmd; i++)
		{
			fprintf(out, "  --%-16s (-%c)  %s\n",
					cmds[i].cmd, cmds[i].op, cmds[i].description);
		}
		fprintf(out, "options:\n");
		for (i = 0; i < countof(shared_options); i++)
		{
			fprintf(out, "  --%-16s (-%c)  %s\n",
					shared_options[i].name, shared_options[i].op,
					shared_options[i].desc);
		}
	}
	else
	{
		fprintf(out, " (--%s/-%c)\n%s\nusage:\n",
				cmds[active].cmd, cmds[active].op, cmds[active].description);
		for (i = 0; i < MAX_LINES && cmds[active].line[i]; i++)
		{
			if (i == 0)
			{
				fprintf(out, "  swanctl --%s %s\n",
						cmds[active].cmd, cmds[active].line[i]);
			}
			else
			{
				fprintf(out, "    %s\n", cmds[active].line[i]);
			}
		}
		fprintf(out, "options:\n");
		for (i = 0; i < MAX_OPTIONS && cmds[active].options[i].name; i++)
		{
			fprintf(out, "  --%-15s (-%c)  %s\n",
					cmds[active].options[i].name, cmds[active].options[i].op,
					cmds[active].options[i].desc);
		}
	}
	if (error)
	{
		fprintf(out, "error: ");
		va_start(args, error);
		vfprintf(out, error, args);
		va_end(args);
		fprintf(out, "\n");
	}
	return error != NULL;
}

/**
 * Dispatch cleanup hook
 */
static void cleanup()
{
	options->destroy(options);
}

/**
 * Open vici connection, call a command
 */
static int call_command(command_t *cmd)
{
	vici_conn_t *conn;
	int ret;

	conn = vici_connect(uri);
	if (!conn)
	{
		ret = errno;
		command_usage("connecting to '%s' URI failed: %s",
					  uri ?: "default", strerror(errno));
		return ret;
	}
	ret = cmd->call(conn);
	vici_disconnect(conn);
	return ret;
}

/*
 * Described in header
 */
int command_init(int c, char *v[])
{
	int op, i;

	argc = c;
	argv = v;

	options = options_create();
	atexit(cleanup);

	active = help_idx = registered;
	command_register((command_t){NULL, 'h', "help", "show usage, version and "
					 "plugin information"});

	build_opts();
	/* handle common options until we find a command */
	if (!process_common_opts(TRUE))
	{
		return command_usage("invalid --options");
	}
	op = command_getopt(NULL);
	for (i = 0; i < MAX_COMMANDS && cmds[i].cmd; i++)
	{
		if (cmds[i].op == op)
		{
			active = i;
			build_opts();
			/* handle common options again, now with specific options loaded */
			if (!process_common_opts(FALSE))
			{
				return command_usage("invalid --options");
			}
			return 0;
		}
	}
	return command_usage(op != EOF ? "invalid command" : NULL);
}


/*
 * Described in header
 */
int command_dispatch()
{
	/* no callback registered for --help and we don't want to connect to the
	 * socket anyway */
	if (active == help_idx)
	{
		return command_usage(NULL);
	}

	if (!uri)
	{
		uri = lib->settings->get_str(lib->settings, "%s.socket",
				lib->settings->get_str(lib->settings, "%s.plugins.vici.socket",
									   NULL, lib->ns), lib->ns);
	}
	return call_command(&cmds[active]);
}
