// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2026, Dell Technologies Inc. or its subsidiaries.
 *
 * Authors: Martin Belanger <Martin.Belanger@dell.com>
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <libnvme.h>

#include <ccan/array_size/array_size.h>

#include "argconfig.h"
#include "cleanup.h"
#include "config-convert.h"
#include "fabrics.h"
#include "global-ctx.h"
#include "nvme-print.h"

#ifdef CONFIG_JSONC
#include "nvme-json.h"

struct legacy_key {
	const char *json_key;
	const char *ini_key;
};

/* Map config.json keys from underscore to hyphen notation. */
static const struct legacy_key int_keys[] = {
	{ "nr_io_queues",     "nr-io-queues" },
	{ "nr_write_queues",  "nr-write-queues" },
	{ "nr_poll_queues",   "nr-poll-queues" },
	{ "queue_size",       "queue-size" },
	{ "keep_alive_tmo",   "keep-alive-tmo" },
	{ "reconnect_delay",  "reconnect-delay" },
	{ "ctrl_loss_tmo",    "ctrl-loss-tmo" },
	{ "fast_io_fail_tmo", "fast-io-fail-tmo" },
	{ "tos",              "tos" },
};

static const struct legacy_key bool_keys[] = {
	{ "duplicate_connect", "duplicate-connect" },
	{ "disable_sqflow",    "disable-sqflow" },
	{ "hdr_digest",        "hdr-digest" },
	{ "data_digest",       "data-digest" },
	{ "tls",               "tls" },
	{ "concat",            "concat" },
};

static const struct legacy_key string_keys[] = {
	{ "tls_key",          "tls-key" },
	{ "tls_key_identity", "tls-key-identity" },
	{ "keyring",          "keyring" },
	{ "dhchap_key",       "kxchap-secret" },
	{ "dhchap_ctrl_key",  "kxchap-ctrl-secret" },
};

static const char *map_key(const struct legacy_key *table, size_t n,
		const char *json_key)
{
	size_t i;

	for (i = 0; i < n; i++)
		if (!strcmp(table[i].json_key, json_key))
			return table[i].ini_key;

	return NULL;
}

#define MAP_KEY(table, json_key) map_key(table, ARRAY_SIZE(table), json_key)

static const char *json_get_string(struct json_object *obj, const char *key)
{
	struct json_object *val = json_object_object_get(obj, key);

	return val ? json_object_get_string(val) : NULL;
}

static bool json_get_bool(struct json_object *obj, const char *key)
{
	struct json_object *val = json_object_object_get(obj, key);

	return val && json_object_get_boolean(val);
}

/* Copy supported tunable and security parameters into @params. */
static void apply_port_params(struct libnvmf_params *params,
		struct json_object *port_obj)
{
	json_object_object_foreach(port_obj, key_str, val_obj) {
		const char *ini_key;
		char buf[32];

		ini_key = MAP_KEY(int_keys, key_str);
		if (ini_key) {
			snprintf(buf, sizeof(buf), "%d",
				 json_object_get_int(val_obj));
			libnvmf_params_set(params, ini_key, buf);
			continue;
		}
		ini_key = MAP_KEY(bool_keys, key_str);
		if (ini_key) {
			libnvmf_params_set(params, ini_key,
					   json_object_get_boolean(val_obj) ?
					   "true" : "false");
			continue;
		}
		ini_key = MAP_KEY(string_keys, key_str);
		if (ini_key)
			libnvmf_params_set(params, ini_key,
					   json_object_get_string(val_obj));
	}
}

/*
 * The KX-HMAC-CHAP secret is defined per (hostnqn, subsysnqn), not per path
 * (NVMe Base Specification 2.4, section 8.3.4.5.7). If a port does not
 * specify a secret, inherit the host-level default. If both are present but
 * differ, keep the port-specific value and log the mismatch.
 */
static void apply_dhchap_default(struct libnvmf_params *params,
		struct json_object *port_obj, const char *host_default)
{
	const char *port_value = json_get_string(port_obj, "dhchap_key");

	if (!host_default)
		return;

	if (!port_value) {
		libnvmf_params_set(params, "kxchap-secret", host_default);
	} else if (strcmp(port_value, host_default)) {
		nvme_show_verbose_info(
			"config convert: dhchap_key differs between host default and one connection; keeping the connection's own value");
	}
}

/*
 * The legacy config.json format predates EPCSD; its boolean "persistent"
 * meant unconditional persistence. Map true to "force", not the new
 * best-effort "auto" default, so migrating an existing config.json doesn't
 * silently change behavior for a connection that was persistent before.
 */
static void apply_dc_persistent(struct libnvmf_params *params,
		struct json_object *port_obj)
{
	struct json_object *val = json_object_object_get(port_obj, "persistent");

	if (val)
		libnvmf_params_set(params, "persistent",
			json_object_get_boolean(val) ? "force" : "no");
}

static int convert_port(struct libnvmf_config_emitter *emitter,
		const char *hostnqn, const char *hostid,
		const char *hostsymname, const char *host_dhchap_key,
		const char *subsysnqn, struct json_object *port_obj)
{
	struct libnvmf_params *params;
	bool is_dc = json_get_bool(port_obj, "discovery");
	int ret;

	params = libnvmf_params_new();
	if (!params)
		return -ENOMEM;

	apply_port_params(params, port_obj);
	apply_dhchap_default(params, port_obj, host_dhchap_key);
	if (is_dc)
		apply_dc_persistent(params, port_obj);

	ret = libnvmf_config_emit_add(emitter, is_dc,
			json_get_string(port_obj, "transport"),
			json_get_string(port_obj, "traddr"),
			json_get_string(port_obj, "trsvcid"),
			subsysnqn,
			json_get_string(port_obj, "host_traddr"),
			json_get_string(port_obj, "host_iface"),
			hostnqn, hostid, params, hostsymname);

	libnvmf_params_free(params);

	/*
	 * A rejected entry (for example, invalid addressing or a conflicting
	 * persona) affects only that entry. Log the error and continue
	 * converting, matching json_parse_port()'s own tolerance. Stop only
	 * if memory allocation fails.
	 */
	if (ret == -ENOMEM)
		return ret;
	if (ret)
		nvme_show_error(
			"config convert: skipping an entry that could not be added: %s",
			libnvme_strerror(-ret));

	return 0;
}

static int convert_subsys(struct libnvmf_config_emitter *emitter,
		const char *hostnqn, const char *hostid,
		const char *hostsymname, const char *host_dhchap_key,
		struct json_object *subsys_obj)
{
	struct json_object *port_array;
	const char *nqn = json_get_string(subsys_obj, "nqn");
	int p, ret;

	/* The well-known discovery NQN is the emitter's default value. */
	if (nqn && (!*nqn || !strcmp(nqn, NVME_DISC_SUBSYS_NAME)))
		nqn = NULL;

	port_array = json_object_object_get(subsys_obj, "ports");
	if (!port_array)
		return 0;

	for (p = 0; p < json_object_array_length(port_array); p++) {
		struct json_object *port_obj =
			json_object_array_get_idx(port_array, p);

		if (!port_obj)
			continue;
		ret = convert_port(emitter, hostnqn, hostid, hostsymname,
				   host_dhchap_key, nqn, port_obj);
		if (ret)
			return ret;
	}

	return 0;
}

static int convert_host(struct libnvmf_config_emitter *emitter,
		struct json_object *host_obj)
{
	struct json_object *subsys_array;
	const char *hostnqn = json_get_string(host_obj, "hostnqn");
	const char *hostid = json_get_string(host_obj, "hostid");
	const char *hostsymname = json_get_string(host_obj, "hostsymname");
	const char *host_dhchap_key = json_get_string(host_obj, "dhchap_key");
	int s, ret;

	subsys_array = json_object_object_get(host_obj, "subsystems");
	if (!subsys_array)
		return 0;

	for (s = 0; s < json_object_array_length(subsys_array); s++) {
		struct json_object *subsys_obj =
			json_object_array_get_idx(subsys_array, s);

		if (!subsys_obj)
			continue;
		ret = convert_subsys(emitter, hostnqn, hostid, hostsymname,
				     host_dhchap_key, subsys_obj);
		if (ret)
			return ret;
	}

	return 0;
}

int nvme_config_convert_json(struct libnvmf_config_emitter *emitter,
		const char *json_file)
{
	struct json_object *json_root, *host_array, *host_obj;
	int h, ret;

	json_root = json_object_from_file(json_file);
	if (!json_root) {
		nvme_show_error("failed to parse %s: %s", json_file,
				 json_util_get_last_err());
		return -EPROTO;
	}

	if (json_object_is_type(json_root, json_type_object)) {
		/* Current format: { "hosts": [ ... ] } */
		host_array = json_object_object_get(json_root, "hosts");
		if (!host_array ||
		    !json_object_is_type(host_array, json_type_array)) {
			nvme_show_error("%s: expected a 'hosts' array",
					 json_file);
			json_object_put(json_root);
			return -EPROTO;
		}
	} else if (json_object_is_type(json_root, json_type_array)) {
		/* Legacy pre-3.0 format: a bare top-level array of hosts. */
		host_array = json_root;
	} else {
		nvme_show_error("%s: expected a JSON object or array",
				 json_file);
		json_object_put(json_root);
		return -EPROTO;
	}

	for (h = 0; h < json_object_array_length(host_array); h++) {
		host_obj = json_object_array_get_idx(host_array, h);
		if (!host_obj)
			continue;
		ret = convert_host(emitter, host_obj);
		if (ret) {
			json_object_put(json_root);
			return ret;
		}
	}

	json_object_put(json_root);

	return 0;
}

#else /* CONFIG_JSONC */

int nvme_config_convert_json(struct libnvmf_config_emitter *emitter,
		const char *json_file)
{
	nvme_show_error(
		"built without json-c; config.json conversion unavailable");
	return -ENOTSUP;
}

#endif /* CONFIG_JSONC */

int nvme_config_convert_discovery_args(struct libnvmf_config_emitter *emitter,
		const struct nvmf_args *fa, const char *persistent)
{
	struct libnvmf_params *params;
	int ret;

	params = libnvmf_params_new();
	if (!params)
		return -ENOMEM;

	nvmf_args_to_params(params, fa);
	if (persistent &&
	    libnvmf_params_set(params, "persistent", persistent)) {
		nvme_show_error(
			"discovery.conf: skipping a line with an invalid persistent value '%s'",
			persistent);
		libnvmf_params_free(params);
		return 0;
	}

	ret = libnvmf_config_emit_add(emitter, true, fa->transport, fa->traddr,
			fa->trsvcid, fa->subsysnqn, fa->host_traddr,
			fa->host_iface, fa->hostnqn, fa->hostid, params, NULL);

	libnvmf_params_free(params);

	/*
	 * A rejected entry affects only that entry. Log the error and
	 * continue converting. Stop only if memory allocation fails.
	 */
	if (ret == -ENOMEM)
		return ret;
	if (ret)
		nvme_show_error(
			"discovery.conf: skipping a line that could not be added: %s",
			libnvme_strerror(-ret));

	return 0;
}

int nvme_config_convert_discovery(struct libnvmf_config_emitter *emitter,
		const char *disc_file)
{
	__cleanup_file FILE *f = NULL;
	static char line[4096];
	int ret;

	f = fopen(disc_file, "r");
	if (!f) {
		nvme_show_error("failed to open %s: %s", disc_file,
				 strerror(errno));
		return -errno;
	}

	while (fgets(line, sizeof(line), f)) {
		ret = nvmf_convert_discovery_line(emitter, line);
		if (ret)
			return ret;
	}

	return 0;
}

/*
 * Best effort. The configuration has already been installed. @path is
 * left untouched so a rollback to a pre-INI version still finds it; the
 * symlink marks it as already converted.
 */
static void mark_converted(const char *path)
{
	__cleanup_free char *dst = NULL;

	if (asprintf(&dst, "%s.converted", path) < 0)
		return;

	if (symlink(path, dst))
		nvme_show_error(
			"converted %s but failed to mark it as converted (%s): %s",
			path, dst, strerror(errno));
}

/*
 * True if the @path.converted symlink exists and resolves. A dangling
 * symlink (its target since removed) makes this return false, but that
 * alone does not mean @path needs converting -- callers must also check
 * whether @path itself exists.
 */
static bool already_converted(const char *path)
{
	__cleanup_free char *converted = NULL;

	if (asprintf(&converted, "%s.converted", path) < 0)
		return false;

	return !access(converted, F_OK);
}

static int install_converted(struct libnvmf_config_emitter *emitter,
		const char *output_file, const char *json_path,
		const char *disc_path, bool converted_json,
		bool converted_disc, bool force)
{
	int ret;

	ret = libnvmf_config_emit_install(emitter, output_file, force);
	if (ret == -EEXIST) {
		nvme_show_error("%s already exists; refusing to overwrite",
				 output_file);
		return ret;
	}
	if (ret) {
		nvme_show_error("failed to write %s: %s", output_file,
				 libnvme_strerror(-ret));
		return ret;
	}

	if (converted_json)
		mark_converted(json_path);
	if (converted_disc)
		mark_converted(disc_path);

	return 0;
}

int nvme_config_convert_auto(struct libnvme_global_ctx *ctx,
		const char *config_file, char **ini_path)
{
	struct libnvmf_config_emitter *emitter;
	const char *json_path = config_file;
	const char *ext;
	bool is_default;
	bool json_exists, json_done;
	bool disc_exists, disc_done;
	bool have_json, have_disc;
	bool converted_json = false, converted_disc = false;
	int ret;

	*ini_path = NULL;

	is_default = !strcmp(config_file, PATH_NVMF_INI) ||
		     !strcmp(config_file, PATH_NVMF_CONFIG);
	if (is_default) {
		json_path = PATH_NVMF_CONFIG;
		*ini_path = strdup(PATH_NVMF_INI);
	} else {
		ext = strrchr(config_file, '.');
		if (!ext || strcmp(ext, ".json")) {
			*ini_path = strdup(config_file);
			return *ini_path ? 0 : -ENOMEM;
		}

		if (asprintf(ini_path, "%.*s.conf",
			     (int)(ext - config_file), config_file) < 0)
			return -ENOMEM;
	}
	if (!*ini_path)
		return -ENOMEM;

	if (!access(*ini_path, F_OK))
		return 0;

	json_exists = !access(json_path, F_OK);
	json_done = already_converted(json_path);
	have_json = json_exists && !json_done;

	disc_exists = is_default && !access(PATH_NVMF_DISC, F_OK);
	disc_done = is_default && already_converted(PATH_NVMF_DISC);
	have_disc = disc_exists && !disc_done;

	if (!have_json && !have_disc) {
		/* Default path: nothing to convert is fine, proceed empty.
		 * Custom path: never existed and never converted is a
		 * real error, not silent-empty.
		 */
		if (!is_default && !json_exists && !json_done) {
			nvme_show_error("%s: no such file", json_path);
			return -ENOENT;
		}
		return 0;
	}

	emitter = libnvmf_config_emit_new(ctx);
	if (!emitter)
		return -ENOMEM;

	if (have_json) {
		ret = nvme_config_convert_json(emitter, json_path);
		if (ret)
			goto out;
		converted_json = true;
	}

	if (have_disc) {
		ret = nvme_config_convert_discovery(emitter, PATH_NVMF_DISC);
		if (ret)
			goto out;
		converted_disc = true;
	}

	ret = install_converted(emitter, *ini_path, json_path, PATH_NVMF_DISC,
				 converted_json, converted_disc, false);
	if (ret)
		goto out;

	nvme_show_error(
		"no %s found; converted legacy %s%s%s to it -- the original is preserved for rollback, marked converted by a *.converted symlink; use %s from now on",
		*ini_path, converted_json ? json_path : "",
		(converted_json && converted_disc) ? " and " : "",
		converted_disc ? PATH_NVMF_DISC : "", *ini_path);

out:
	libnvmf_config_emit_free(emitter);

	return ret;
}

int nvme_config_convert(const char *desc, int argc, char **argv)
{
	char *config_file = NULL;
	char *output_file = NULL;
	const char *target;
	const char *json_path;
	bool force = false;
	bool converted_json = false, converted_disc = false;
	bool json_already_done = false, disc_already_done = false;
	__cleanup_nvme_global_ctx struct libnvme_global_ctx *ctx = NULL;
	struct libnvmf_config_emitter *emitter = NULL;
	int ret;

	OPT_ARGS(opts) = {
		OPT_STRING("config", 'J', "FILE", &config_file,
			   "convert this JSON file (default: config.json)"),
		OPT_STRING("output", 'o', "FILE", &output_file,
			   "write result here (default: nvme-fabrics.conf)"),
		OPT_FLAG("force", 0, &force,
			 "overwrite an existing target"),
		OPT_END()
	};

	ret = parse_args(argc, argv, desc, opts);
	if (ret)
		return ret;

	ret = nvme_create_global_ctx(&ctx);
	if (ret)
		return ret;

	emitter = libnvmf_config_emit_new(ctx);
	if (!emitter)
		return -ENOMEM;

	json_path = config_file ? config_file : PATH_NVMF_CONFIG;
	if (already_converted(json_path)) {
		json_already_done = true;
	} else if (!access(json_path, F_OK)) {
		ret = nvme_config_convert_json(emitter, json_path);
		if (ret)
			goto out;
		converted_json = true;
	} else if (config_file) {
		/*
		 * An explicit --config to a file that neither exists nor was
		 * ever converted: let the JSON parser produce its own
		 * "failed to parse" error instead of silently no-op'ing,
		 * since this was an explicit ask.
		 */
		ret = nvme_config_convert_json(emitter, json_path);
		if (ret)
			goto out;
		converted_json = true;
	}

	if (already_converted(PATH_NVMF_DISC)) {
		disc_already_done = true;
	} else if (!access(PATH_NVMF_DISC, F_OK)) {
		ret = nvme_config_convert_discovery(emitter, PATH_NVMF_DISC);
		if (ret)
			goto out;
		converted_disc = true;
	}

	if (!converted_json && !converted_disc) {
		if (json_already_done || disc_already_done) {
			nvme_show_result("already converted; nothing to do");
			ret = 0;
			goto out;
		}
		nvme_show_error("nothing to convert: neither %s nor %s exists",
				 json_path, PATH_NVMF_DISC);
		ret = -ENOENT;
		goto out;
	}

	target = output_file ? output_file : PATH_NVMF_INI;
	ret = install_converted(emitter, target, json_path, PATH_NVMF_DISC,
				 converted_json, converted_disc, force);
	if (!ret) {
		nvme_show_result(
			"converted legacy %s%s%s to %s -- the original is preserved for rollback, marked converted by a *.converted symlink",
			converted_json ? json_path : "",
			(converted_json && converted_disc) ? " and " : "",
			converted_disc ? PATH_NVMF_DISC : "", target);
	}

out:
	libnvmf_config_emit_free(emitter);

	return ret;
}

/*
 * Report @path's legacy-conversion state in one line, if there is
 * anything to report. Returns false, printing nothing, when neither
 * @path nor its marker exists.
 *
 * The "*.converted" marker (see mark_converted()) is a symlink whose
 * target is @path itself, so whether it resolves is not independent
 * information from whether @path exists -- it is the same fact observed
 * two ways. That leaves four real states: no file and no marker; a file
 * not yet converted; a converted file (marker resolves, @path exists by
 * construction); and a dangling marker left over after @path was removed
 * post-conversion (@path absent by construction).
 */
static bool report_legacy_status(const char *path)
{
	__cleanup_free char *marker = NULL;
	struct stat sb;
	bool file_exists;
	bool marker_present;

	if (asprintf(&marker, "%s.converted", path) < 0)
		return false;

	file_exists = !access(path, F_OK);
	marker_present = !lstat(marker, &sb);

	if (!marker_present) {
		if (!file_exists)
			return false;

		nvme_show_result("%s: present, not yet converted", path);
		return true;
	}

	if (!file_exists) {
		nvme_show_result(
			"%s: not present (a stale %s marker exists -- safe to delete)",
			path, marker);
		return true;
	}

	nvme_show_result(
		"%s: present, already converted (%s exists) -- delete both once rollback is no longer a concern",
		path, marker);
	nvme_show_result(
		"note: if %s was replaced after conversion (for example, a package rollback followed by reinstall), this marker may be stale; verify its contents against the INI configuration before deleting either file",
		path);

	return true;
}

int nvme_config_status(const char *desc, int argc, char **argv)
{
	OPT_ARGS(opts) = {
		OPT_END()
	};
	bool json_found, disc_found;
	int ret;

	ret = parse_args(argc, argv, desc, opts);
	if (ret)
		return ret;

	json_found = report_legacy_status(PATH_NVMF_CONFIG);
	disc_found = report_legacy_status(PATH_NVMF_DISC);

	if (!json_found && !disc_found) {
		nvme_show_result(
			"no legacy configuration found; nothing to convert");
		return 0;
	}

	return 1;
}
