#include "config.h"
#include <assert.h>
#include <bitcoin/chainparams.h>
#include <ccan/err/err.h>
#include <ccan/opt/opt.h>
#include <ccan/opt/private.h>
#include <ccan/tal/grab_file/grab_file.h>
#include <ccan/tal/path/path.h>
#include <ccan/tal/str/str.h>
#include <common/configdir.h>
#include <common/configvar.h>
#include <common/memleak.h>
#include <common/utils.h>
#include <common/version.h>

int opt_exitcode = 1;

/* The regrettable globals */
static const tal_t *options_ctx;
static struct configvar *current_cv;

/* Override a tal string; frees the old one. */
char *opt_set_talstr(const char *arg, char **p)
{
	tal_free(*p);
	return opt_set_charp(tal_strdup(options_ctx, arg), p);
}

static char *opt_set_abspath(const char *arg, char **p)
{
	tal_free(*p);
	return opt_set_charp(path_join(options_ctx, take(path_cwd(NULL)), arg),
			     p);
}

/* Tal wrappers for opt. */
static void *opt_allocfn(size_t size)
{
	return notleak(tal_arr(NULL, char, size));
}

static void *tal_reallocfn(void *ptr, size_t size)
{
	if (!ptr)
		return opt_allocfn(size);
	tal_resize_(&ptr, 1, size, false);
	return ptr;
}

static void tal_freefn(void *ptr)
{
	tal_free(ptr);
}

static struct configvar **gather_file_configvars(const tal_t *ctx,
						 enum configvar_src src,
						 const char *filename,
						 bool must_exist,
						 char **setconfig_file,
						 size_t include_depth)
{
	char *contents, **lines;
	struct configvar **cvs = tal_arr(ctx, struct configvar *, 0);

	contents = grab_file_str(tmpctx, filename);

	/* The default config doesn't have to exist, but if the config was
	 * specified on the command line it has to exist. */
	if (!contents) {
		if (must_exist)
			err(1, "Opening and reading %s", filename);
		return cvs;
	}

	/* First .setconfig file is used for setconfig command */
	if (setconfig_file
	    && *setconfig_file == NULL
	    && strends(filename, ".setconfig")) {
		*setconfig_file = tal_strdup(ctx, filename);
	}

	/* Break into lines. */
	lines = tal_strsplit(contents, contents, "\r\n", STR_EMPTY_OK);
	for (size_t i = 0; i < tal_count(lines) - 1; i++) {
		/* Comments & blank lines*/
		if (strstarts(lines[i], "#") || streq(lines[i], ""))
			continue;

		if (strstarts(lines[i], "include ")) {
			const char *included = lines[i] + strlen("include ");
			struct configvar **sub;

			if (include_depth > 100)
				errx(1, "Include loop with %s and %s", filename, included);

			/* If relative, it's relative to current config file */
			sub = gather_file_configvars(NULL,
						     src,
						     path_join(tmpctx,
							       take(path_dirname(NULL, filename)),
							       included),
						     true,
						     setconfig_file,
						     include_depth + 1);
			cvs = configvar_join(ctx, take(cvs), take(sub));
			continue;
		}

		tal_arr_expand(&cvs,
			       configvar_new(cvs, src, filename, i+1, lines[i]));
	}
	return cvs;
}

static char *default_base_configdir(const tal_t *ctx)
{
	char *path;
	const char *env = getenv("HOME");
	if (!env)
		return path_cwd(ctx);

	path = path_join(ctx, env, ".lightning");
	return path;
}

static char *default_rpcfile(const tal_t *ctx)
{
	return tal_strdup(ctx, "lightning-rpc");
}

static char *opt_set_network(const char *arg, void *unused)
{
	assert(arg != NULL);

	/* Ignore if called directly from opt (e.g. lightning-cli) */
	if (!current_cv)
		return NULL;

	if (current_cv->src == CONFIGVAR_NETWORK_CONF)
		return "not permitted in network-specific configuration files";
	/* Set the global chainparams instance */
	chainparams = chainparams_for_network(arg);
	if (!chainparams)
		return tal_fmt(NULL, "Unknown network name '%s'", arg);
	return NULL;
}

static char *opt_set_specific_network(const char *network)
{
	return opt_set_network(network, NULL);
}

static bool opt_show_network(char *buf, size_t len, const void *unused)
{
	snprintf(buf, len, "%s", chainparams->network_name);
	return true;
}

static char *opt_set_config_filename(const char *arg, char **p)
{
	/* Ignore if called directly from opt (e.g. lightning-cli) */
	if (!current_cv)
		return NULL;

	if (current_cv->src == CONFIGVAR_CMDLINE)
		return opt_set_abspath(arg, p);
	return "not permitted in configuration files";
}

static char *opt_set_lightning_dir(const char *arg, char **p)
{
	/* Ignore if called directly from opt (e.g. lightning-cli) */
	if (!current_cv)
		return NULL;

	if (current_cv->src == CONFIGVAR_CMDLINE
	    || current_cv->src == CONFIGVAR_EXPLICIT_CONF)
		return opt_set_abspath(arg, p);
	return "not permitted in implicit configuration files";
}

void setup_option_allocators(void)
{
	/*~ These functions make ccan/opt use tal for allocations */
	opt_set_alloc(opt_allocfn, tal_reallocfn, tal_freefn);
}

static void parse_configvars(struct configvar **cvs,
			     bool early,
			     bool full_knowledge,
			     bool developer)
{
	for (size_t i = 0; i < tal_count(cvs); i++) {
		const char *problem;
		bool should_know;

		should_know = full_knowledge;
		/* We should always know cmdline args in final parse */
		if (!early && cvs[i]->src == CONFIGVAR_CMDLINE)
			should_know = true;

		current_cv = cvs[i];
		problem = configvar_parse(cvs[i],
					  early,
					  should_know,
					  developer);
		current_cv = NULL;
		if (!problem)
			continue;

		if (cvs[i]->file) {
			errx(opt_exitcode, "Config file %s line %u: %s: %s",
			     cvs[i]->file, cvs[i]->linenum,
			     cvs[i]->configline, problem);
		} else {
			errx(opt_exitcode, "--%s: %s", cvs[i]->configline, problem);
		}
	}
}

static void finished_arg(int *argc, char **argv, size_t *idx,
			 bool remove_args)
{
	if (!remove_args) {
		(*idx)++;
		return;
	}
	memmove(argv + *idx, argv + 1 + *idx, (*argc - *idx) * sizeof(char *));
	(*argc)--;
}

/* Now all options are known, we can turn cmdline into configvars */
static struct configvar **gather_cmdline_args(const tal_t *ctx,
					      int *argc, char **argv,
					      bool remove_args)
{
	struct configvar **cvs = tal_arr(ctx, struct configvar *, 0);

	assert(argv[*argc] == NULL);
	for (size_t i = 1; argv[i];) {
		struct opt_table *ot;
		const char *configline, *arg, *optarg;
		enum configvar_src src;
		bool extra_arg;

		/* End of options? */
		if (streq(argv[i], "--"))
			break;

		if (!strstarts(argv[i], "-")) {
			i++;
			continue;
		}

		if (strstarts(argv[i], "--")) {
			arg = argv[i] + 2;
			ot = opt_find_long(arg, &optarg);
			src = CONFIGVAR_CMDLINE;
		} else {
			/* FIXME: We don't handle multiple short
			 * options here! */
			arg = argv[i] + 1;
			ot = opt_find_short(arg[0]);
			optarg = NULL;
			src = CONFIGVAR_CMDLINE_SHORT;
		}
		if (ot) {
			extra_arg = (ot->type & OPT_HASARG) && !optarg;
		} else {
			/* Unknown (yet!).  Guess if next arg is for this! */
			extra_arg = ((src == CONFIGVAR_CMDLINE_SHORT
				      || !strchr(arg, '='))
				     && argv[i+1]
				     && !strstarts(argv[i+1], "-"));
		}
		finished_arg(argc, argv, &i, remove_args);
		/* We turn `--foo bar` into `--foo=bar` here */
		if (extra_arg) {
			configline = tal_fmt(tmpctx, "%s=%s", arg, argv[i]);
			finished_arg(argc, argv, &i, remove_args);
		} else {
			configline = arg;
		}
		tal_arr_expand(&cvs, configvar_new(cvs, src,
						   NULL, 0, configline));
	}
	assert(argv[*argc] == NULL);
	return cvs;
}

void minimal_config_opts(const tal_t *ctx,
			 int argc, char *argv[],
			 char **config_filename,
			 char **basedir,
			 char **config_netdir,
			 char **rpc_filename)
{
	initial_config_opts(tmpctx, &argc, argv, false,
			    config_filename,
			    basedir,
			    config_netdir,
			    rpc_filename, NULL);
	tal_steal(ctx, *config_filename);
	tal_steal(ctx, *basedir);
	tal_steal(ctx, *config_netdir);
	tal_steal(ctx, *rpc_filename);
}

struct configvar **initial_config_opts(const tal_t *ctx,
				       int *argc, char *argv[],
				       bool remove_args,
				       char **config_filename,
				       char **config_basedir,
				       char **config_netdir,
				       char **rpc_filename,
				       char **setconfig_file)
{
	struct configvar **cmdline_cvs, **config_cvs, **cvs;

	options_ctx = ctx;

	/* This helps opt_usage. */
	opt_argv0 = argv[0];

	/* Default chain (a global) is bitcoin. */
	chainparams = chainparams_for_network("bitcoin");

	/* First, they could specify a config, or base dir. */
	*config_filename = NULL;
	opt_register_early_arg("--conf=<file>",
			       opt_set_config_filename,
			       /* Doesn't show if it's NULL! */
			       opt_show_charp,
			       config_filename,
			       "Specify configuration file");
	*config_basedir = default_base_configdir(ctx);
	opt_register_early_arg("--lightning-dir=<dir>",
			       opt_set_lightning_dir, opt_show_charp,
			       config_basedir,
			       "Set base directory: network-specific subdirectory is under here");
	opt_register_early_arg("--network", opt_set_network, opt_show_network,
			       NULL,
			       "Select the network parameters (bitcoin, testnet,"
			       " signet, or regtest)");
	opt_register_early_noarg("--testnet",
				 opt_set_specific_network, "testnet",
				 "Alias for --network=testnet");
	opt_register_early_noarg("--testnet4",
				 opt_set_specific_network, "testnet4",
				 "Alias for --network=testnet4");
	opt_register_early_noarg("--signet",
				 opt_set_specific_network, "signet",
				 "Alias for --network=signet");
	opt_register_early_noarg("--mainnet",
				 opt_set_specific_network, "bitcoin",
				 "Alias for --network=bitcoin");
	opt_register_early_noarg("--regtest",
				 opt_set_specific_network, "regtest",
				 "Alias for --network=regtest");
	/* Handle --version (and exit) here too */
	opt_register_version();

	/* Allow them to override rpc-file too. */
	*rpc_filename = default_rpcfile(ctx);
	opt_register_early_arg("--rpc-file", opt_set_talstr, opt_show_charp,
			       rpc_filename,
			       "Set JSON-RPC socket (or /dev/tty)");

	cmdline_cvs = gather_cmdline_args(tmpctx, argc, argv, remove_args);
	parse_configvars(cmdline_cvs, true, false, false);

	if (setconfig_file)
		*setconfig_file = NULL;

	/* Base default or direct config can set network */
	if (*config_filename) {
		config_cvs = gather_file_configvars(NULL,
						    CONFIGVAR_EXPLICIT_CONF,
						    *config_filename, true,
						    setconfig_file, 0);
	} else {
		struct configvar **base_cvs, **net_cvs;
		char *dir = path_join(tmpctx, take(path_cwd(NULL)), *config_basedir);
		/* Optional: .lightning/config */
		base_cvs = gather_file_configvars(tmpctx,
						  CONFIGVAR_BASE_CONF,
						  path_join(tmpctx, dir, "config"),
						  false,
						  setconfig_file, 0);
		/* This might set network! */
		parse_configvars(configvar_join(tmpctx, base_cvs, cmdline_cvs),
				 true, false, false);

		/* Now, we can get network config */
		dir = path_join(tmpctx, dir, chainparams->network_name);
		net_cvs = gather_file_configvars(tmpctx,
						 CONFIGVAR_NETWORK_CONF,
						 path_join(tmpctx, dir, "config"),
						 false, setconfig_file, 0);
		config_cvs = configvar_join(NULL, take(base_cvs), take(net_cvs));
	}
	cvs = configvar_join(ctx, take(config_cvs), cmdline_cvs);

	/* This will be called again, once caller has added their own
	 * early vars! */
	parse_configvars_early(cvs, false);

	*config_netdir
		= path_join(NULL, *config_basedir, chainparams->network_name);

	/* Make sure it's absolute */
	*config_netdir = path_join(ctx, take(path_cwd(NULL)), take(*config_netdir));
	return cvs;
}

void parse_configvars_early(struct configvar **cvs, bool developer)
{
	parse_configvars(cvs, true, false, developer);
}

void parse_configvars_final(struct configvar **cvs,
			    bool full_knowledge,
			    bool developer)
{
	parse_configvars(cvs, false, full_knowledge, developer);
	configvar_finalize_overrides(cvs);
}

bool is_restricted_ignored(const void *fn)
{
	return fn == opt_set_specific_network;
}

bool is_restricted_print_if_nonnull(const void *fn)
{
	return fn == opt_set_config_filename;
}
