/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <stddef.h>
#include <sys/stat.h>

#include "contrib/openbsd/strlcat.h"
#include "knot/conf/conf.h"
#include "knot/common/log.h"
#include "utils/knotc/commands.h"
#include "utils/knotc/process.h"

static const cmd_desc_t *get_cmd_desc(const char *command)
{
	/* Find requested command. */
	const cmd_desc_t *desc = cmd_table;
	while (desc->name != NULL) {
		if (strcmp(desc->name, command) == 0) {
			break;
		}
		desc++;
	}
	if (desc->name == NULL) {
		log_error("invalid command '%s'", command);
		return NULL;
	}

	return desc;
}

static bool get_cmd_force_flag(const char *arg)
{
	if (strcmp(arg, "-f") == 0 || strcmp(arg, "--force") == 0) {
		return true;
	}
	return false;
}

static bool get_cmd_blocking_flag(const char *arg)
{
	if (strcmp(arg, "-b") == 0 || strcmp(arg, "--blocking") == 0) {
		return true;
	}
	return false;
}

int set_config(const cmd_desc_t *desc, params_t *params, bool compl)
{
	/* Reset the configuration paths (needed in the interactive mode). */
	params->config = params->orig_config;
	params->confdb = params->orig_confdb;

	if (!compl && (desc->flags & CMD_FLOG_MORE)) {
		log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY,
			       LOG_MASK(LOG_INFO) | LOG_MASK(LOG_NOTICE));
	} else {
		log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, 0);
	}

	if (params->config != NULL && params->confdb != NULL) {
		log_error("ambiguous configuration source");
		return KNOT_EINVAL;
	}

	/* Mask relevant flags. */
	cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE);
	cmd_flag_t mod_flags = desc->flags & (CMD_FOPT_MOD | CMD_FREQ_MOD);

	/* Choose the optimal config source. */
	struct stat st;
	bool import = false;
	if (flags == CMD_FNONE && params->socket != NULL) {
		/* Control operation, known socket, skip configuration. */
		return KNOT_EOK;
	} else if (params->confdb != NULL) {
		import = false;
	} else if (flags == CMD_FWRITE) {
		import = false;
		params->confdb = CONF_DEFAULT_DBDIR;
	} else if (params->config != NULL){
		import = true;
	} else if (conf_db_exists(CONF_DEFAULT_DBDIR)) {
		import = false;
		params->confdb = CONF_DEFAULT_DBDIR;
	} else if (stat(CONF_DEFAULT_FILE, &st) == 0) {
		import = true;
		params->config = CONF_DEFAULT_FILE;
	} else if (flags != CMD_FNONE) {
		log_error("no configuration source available");
		return KNOT_EINVAL;
	}

	const char *src = import ? params->config : params->confdb;
	log_debug("%s '%s'", import ? "config" : "confdb",
	          (src != NULL) ? src : "empty");

	/* Prepare config flags. */
	conf_flag_t conf_flags = CONF_FNOHOSTNAME;
	if (params->confdb != NULL && !(flags & CMD_FWRITE)) {
		conf_flags |= CONF_FREADONLY;
	}
	if (import || mod_flags & CMD_FOPT_MOD) {
		conf_flags |= CONF_FOPTMODULES;
	} else if (mod_flags & CMD_FREQ_MOD) {
		conf_flags |= CONF_FREQMODULES;
	}

	/* Open confdb. */
	conf_t *new_conf = NULL;
	int ret = conf_new(&new_conf, conf_schema, params->confdb,
	                   params->max_conf_size, conf_flags);
	if (ret != KNOT_EOK) {
		log_error("failed to open configuration database '%s' (%s)",
		          (params->confdb != NULL) ? params->confdb : "",
		          knot_strerror(ret));
		return ret;
	}

	/* Import the config file. */
	if (import) {
		ret = conf_import(new_conf, params->config, IMPORT_FILE);
		if (ret != KNOT_EOK) {
			log_error("failed to load configuration file '%s' (%s)",
			          params->config, knot_strerror(ret));
			conf_free(new_conf);
			return ret;
		}
	}

	/* Update to the new config. */
	conf_update(new_conf, CONF_UPD_FNONE);

	return KNOT_EOK;
}

int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc)
{
	if (desc == NULL) {
		*ctl = NULL;
		return KNOT_EINVAL;
	}

	/* Mask relevant flags. */
	cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE);

	/* Check if control socket is required. */
	if (flags != CMD_FNONE) {
		*ctl = NULL;
		return KNOT_EOK;
	}

	/* Get control socket path. */
	char *path = NULL;
	if (socket != NULL) {
		path = strdup(socket);
	} else {
		conf_val_t listen_val = conf_get(conf(), C_CTL, C_LISTEN);
		conf_val_t rundir_val = conf_get(conf(), C_SRV, C_RUNDIR);
		char *rundir = conf_abs_path(&rundir_val, NULL);
		path = conf_abs_path(&listen_val, rundir);
		free(rundir);
	}
	if (path == NULL) {
		log_error("empty control socket path");
		return KNOT_EINVAL;
	}

	log_debug("socket '%s'", path);

	*ctl = knot_ctl_alloc();
	if (*ctl == NULL) {
		free(path);
		return KNOT_ENOMEM;
	}

	knot_ctl_set_timeout(*ctl, timeout);

	int ret = knot_ctl_connect(*ctl, path);
	if (ret != KNOT_EOK) {
		knot_ctl_free(*ctl);
		*ctl = NULL;
		log_error("failed to connect to socket '%s' (%s)", path,
		          knot_strerror(ret));
		free(path);
		return ret;
	}

	free(path);

	return KNOT_EOK;
}

void unset_ctl(knot_ctl_t *ctl)
{
	if (ctl == NULL) {
		return;
	}

	int ret = knot_ctl_send(ctl, KNOT_CTL_TYPE_END, NULL);
	if (ret != KNOT_EOK && ret != KNOT_ECONN) {
		log_error("failed to finish control (%s)", knot_strerror(ret));
	}

	knot_ctl_close(ctl);
	knot_ctl_free(ctl);
}

int process_cmd(int argc, const char **argv, params_t *params)
{
	if (argc == 0) {
		return KNOT_ENOTSUP;
	}

	/* Check the command name. */
	const cmd_desc_t *desc = get_cmd_desc(argv[0]);
	if (desc == NULL) {
		return KNOT_ENOENT;
	}

	/* Check for exit. */
	if (desc->fcn == NULL) {
		return KNOT_CTL_ESTOP;
	}

	/* Set up the configuration. */
	int ret = set_config(desc, params, false);
	if (ret != KNOT_EOK) {
		return ret;
	}

	/* Prepare command parameters. */
	cmd_args_t args = {
		.desc = desc,
		.argc = argc - 1,
		.argv = argv + 1,
		.force = params->force,
		.extended = params->extended,
		.color = params->color,
		.color_force = params->color_force,
		.blocking = params->blocking
	};

	/* Check for special flags after command. */
	while (args.argc > 0) {
		if (get_cmd_force_flag(args.argv[0])) {
			args.force = true;
			args.argc--;
			args.argv++;
		} else if (get_cmd_blocking_flag(args.argv[0])) {
			args.blocking = true;
			args.argc--;
			args.argv++;
		} else {
			break;
		}
	}

	/* Prepare flags parameter. */
	if (args.force) {
		strlcat(args.flags, CTL_FLAG_FORCE, sizeof(args.flags));
	}
	if (args.blocking) {
		strlcat(args.flags, CTL_FLAG_BLOCKING, sizeof(args.flags));
	}

	/* Set control interface if necessary. */
	int cmd_timeout = params->timeout != -1 ? params->timeout : DEFAULT_CTL_TIMEOUT_MS;
	if (args.blocking && params->timeout == -1) {
		cmd_timeout = 0;
	}
	ret = set_ctl(&args.ctl, params->socket, cmd_timeout, desc);
	if (ret != KNOT_EOK) {
		conf_update(NULL, CONF_UPD_FNONE);
		return ret;
	}

	/* Execute the command. */
	ret = desc->fcn(&args);

	/* Cleanup */
	unset_ctl(args.ctl);
	conf_update(NULL, CONF_UPD_FNONE);

	return ret;
}
