/*
 * (C) 2010 by Andreas Eversberg <jolly@eversberg.eu>
 * (C) 2023 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 *
 * All Rights Reserved
 *
 * 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.
 *
 */

#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>

#include <osmocom/core/utils.h>
#include <osmocom/gsm/gsm48.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/signal.h>
#include <osmocom/core/gsmtap.h>
#include <osmocom/core/gsmtap_util.h>
#include <osmocom/crypt/auth.h>
#include <osmocom/vty/cpu_sched_vty.h>
#include <osmocom/vty/telnet_interface.h>
#include <osmocom/vty/stats.h>
#include <osmocom/vty/misc.h>

#include <osmocom/bb/common/vty.h>
#include <osmocom/bb/common/l23_app.h>
#include <osmocom/bb/common/osmocom_data.h>
#include <osmocom/bb/common/ms.h>
#include <osmocom/bb/common/networks.h>
#include <osmocom/bb/common/gps.h>
#include <osmocom/bb/common/l1l2_interface.h>

extern struct llist_head active_connections; /* libosmocore */

bool l23_vty_reading = false;

bool l23_vty_hide_default = false;

static struct cmd_node ms_node = {
	MS_NODE,
	"%s(ms)# ",
	1
};

static struct cmd_node gsmtap_node = {
	GSMTAP_NODE,
	"%s(gsmtap)# ",
	1
};

struct cmd_node testsim_node = {
	TESTSIM_NODE,
	"%s(test-sim)# ",
	1
};

static void l23_vty_restart_required_warn(struct vty *vty, struct osmocom_ms *ms)
{
	if (l23_vty_reading)
		return;
	if (ms->shutdown != MS_SHUTDOWN_NONE)
		return;
	vty_out(vty, "You must restart MS '%s' ('shutdown / no shutdown') for "
		"change to take effect!%s", ms->name, VTY_NEWLINE);
}

struct osmocom_ms *l23_vty_get_ms(const char *name, struct vty *vty)
{
	struct osmocom_ms *ms;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, name)) {
			if (ms->shutdown != MS_SHUTDOWN_NONE) {
				vty_out(vty, "MS '%s' is admin down.%s", name,
					VTY_NEWLINE);
				return NULL;
			}
			return ms;
		}
	}
	vty_out(vty, "MS name '%s' does not exist.%s", name, VTY_NEWLINE);

	return NULL;
}

void l23_vty_ms_notify(struct osmocom_ms *ms, const char *fmt, ...)
{
	struct telnet_connection *connection;
	char buffer[1000];
	va_list args;
	struct vty *vty;

	if (fmt) {
		va_start(args, fmt);
		vsnprintf(buffer, sizeof(buffer) - 1, fmt, args);
		buffer[sizeof(buffer) - 1] = '\0';
		va_end(args);

		if (!buffer[0])
			return;
	}

	llist_for_each_entry(connection, &active_connections, entry) {
		vty = connection->vty;
		if (!vty)
			continue;
		if (!fmt) {
			vty_out(vty, "%s%% (MS %s)%s", VTY_NEWLINE, ms->name,
				VTY_NEWLINE);
			continue;
		}
		if (buffer[strlen(buffer) - 1] == '\n') {
			buffer[strlen(buffer) - 1] = '\0';
			vty_out(vty, "%% %s%s", buffer, VTY_NEWLINE);
			buffer[strlen(buffer)] = '\n';
		} else
			vty_out(vty, "%% %s", buffer);
	}
}

void l23_vty_printf(void *priv, const char *fmt, ...)
{
	char buffer[1000];
	struct vty *vty = priv;
	va_list args;

	va_start(args, fmt);
	vsnprintf(buffer, sizeof(buffer) - 1, fmt, args);
	buffer[sizeof(buffer) - 1] = '\0';
	va_end(args);

	if (buffer[0]) {
		if (buffer[strlen(buffer) - 1] == '\n') {
			buffer[strlen(buffer) - 1] = '\0';
			vty_out(vty, "%s%s", buffer, VTY_NEWLINE);
		} else
			vty_out(vty, "%s", buffer);
	}
}

/* placeholder for layer23 shared MS info to be dumped */
void l23_ms_dump(struct osmocom_ms *ms, struct vty *vty)
{
	struct gsm_settings *set = &ms->settings;
	char *service = "";

	if (!ms->started)
		service = ", radio is not started";
	else if (ms->mmlayer.state == GSM48_MM_ST_NULL) {
		service = ", MM connection not yet set up";
	} else if (ms->mmlayer.state == GSM48_MM_ST_MM_IDLE) {
		/* current MM idle state */
		switch (ms->mmlayer.substate) {
		case GSM48_MM_SST_NORMAL_SERVICE:
		case GSM48_MM_SST_PLMN_SEARCH_NORMAL:
			service = ", service is normal";
			break;
		case GSM48_MM_SST_LOC_UPD_NEEDED:
		case GSM48_MM_SST_ATTEMPT_UPDATE:
			service = ", service is limited (pending)";
			break;
		case GSM48_MM_SST_NO_CELL_AVAIL:
			service = ", service is unavailable";
			break;
		default:
			if (ms->subscr.sim_valid)
				service = ", service is limited";
			else
				service = ", service is limited "
					"(IMSI detached)";
			break;
		}
	} else
		service = ", MM connection active";

	vty_out(vty, "MS '%s' is %s%s%s%s", ms->name,
		(ms->shutdown != MS_SHUTDOWN_NONE) ? "administratively " : "",
		(ms->shutdown != MS_SHUTDOWN_NONE || !ms->started) ? "down" : "up",
		(ms->shutdown == MS_SHUTDOWN_NONE) ? service : "",
		VTY_NEWLINE);

	vty_out(vty, " IMEI: %s%s", set->imei, VTY_NEWLINE);
	vty_out(vty, " IMEISV: %s%s", set->imeisv, VTY_NEWLINE);
	if (set->imei_random)
		vty_out(vty, " IMEI generation: random (%d trailing "
			"digits)%s", set->imei_random, VTY_NEWLINE);
	else
		vty_out(vty, " IMEI generation: fixed%s", VTY_NEWLINE);
}

/* CONFIG NODE: */
DEFUN(cfg_hide_default, cfg_hide_default_cmd, "hide-default",
	"Hide most default values in config to make it more compact")
{
	l23_vty_hide_default = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_hide_default, cfg_no_hide_default_cmd, "no hide-default",
	NO_STR "Show default values in config")
{
	l23_vty_hide_default = 0;

	return CMD_SUCCESS;
}

DEFUN(show_support, show_support_cmd, "show support [MS_NAME]",
	SHOW_STR "Display information about MS support\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	if (argc) {
		ms = l23_vty_get_ms(argv[0], vty);
		if (!ms)
			return CMD_WARNING;
		gsm_support_dump(ms, l23_vty_printf, vty);
	} else {
		llist_for_each_entry(ms, &ms_list, entity) {
			gsm_support_dump(ms, l23_vty_printf, vty);
			vty_out(vty, "%s", VTY_NEWLINE);
		}
	}

	return CMD_SUCCESS;
}

DEFUN(show_subscr, show_subscr_cmd, "show subscriber [MS_NAME]",
	SHOW_STR "Display information about subscriber\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	if (argc) {
		ms = l23_vty_get_ms(argv[0], vty);
		if (!ms)
			return CMD_WARNING;
		gsm_subscr_dump(&ms->subscr, l23_vty_printf, vty);
	} else {
		llist_for_each_entry(ms, &ms_list, entity) {
			if (ms->shutdown == MS_SHUTDOWN_NONE) {
				gsm_subscr_dump(&ms->subscr, l23_vty_printf, vty);
				vty_out(vty, "%s", VTY_NEWLINE);
			}
		}
	}

	return CMD_SUCCESS;
}

/* "gsmtap" config */
gDEFUN(l23_cfg_gsmtap, l23_cfg_gsmtap_cmd, "gsmtap",
	"Configure GSMTAP\n")
{
	vty->node = GSMTAP_NODE;
	return CMD_SUCCESS;
}

static const struct value_string gsmtap_categ_gprs_names[] = {
	{ L23_GSMTAP_GPRS_C_DL_UNKNOWN,		"dl-unknown" },
	{ L23_GSMTAP_GPRS_C_DL_DUMMY,		"dl-dummy" },
	{ L23_GSMTAP_GPRS_C_DL_CTRL,		"dl-ctrl" },
	{ L23_GSMTAP_GPRS_C_DL_DATA_GPRS,	"dl-data-gprs" },
	{ L23_GSMTAP_GPRS_C_DL_DATA_EGPRS,	"dl-data-egprs" },
	{ L23_GSMTAP_GPRS_C_UL_UNKNOWN,		"ul-unknown" },
	{ L23_GSMTAP_GPRS_C_UL_DUMMY,		"ul-dummy" },
	{ L23_GSMTAP_GPRS_C_UL_CTRL,		"ul-ctrl" },
	{ L23_GSMTAP_GPRS_C_UL_DATA_GPRS,	"ul-data-gprs" },
	{ L23_GSMTAP_GPRS_C_UL_DATA_EGPRS,	"ul-data-egprs" },
	{ 0, NULL }
};

static const struct value_string gsmtap_categ_gprs_help[] = {
	{ L23_GSMTAP_GPRS_C_DL_UNKNOWN,		"Unknown / Unparseable / Erroneous Downlink Blocks" },
	{ L23_GSMTAP_GPRS_C_DL_DUMMY,		"Downlink Dummy Blocks" },
	{ L23_GSMTAP_GPRS_C_DL_CTRL,		"Downlink Control Blocks" },
	{ L23_GSMTAP_GPRS_C_DL_DATA_GPRS,	"Downlink Data Blocks (GPRS)" },
	{ L23_GSMTAP_GPRS_C_DL_DATA_EGPRS,	"Downlink Data Blocks (EGPRS)" },
	{ L23_GSMTAP_GPRS_C_UL_UNKNOWN,		"Unknown / Unparseable / Erroneous Downlink Blocks" },
	{ L23_GSMTAP_GPRS_C_UL_DUMMY,		"Uplink Dummy Blocks" },
	{ L23_GSMTAP_GPRS_C_UL_CTRL,		"Uplink Control Blocks" },
	{ L23_GSMTAP_GPRS_C_UL_DATA_GPRS,	"Uplink Data Blocks (GPRS)" },
	{ L23_GSMTAP_GPRS_C_UL_DATA_EGPRS,	"Uplink Data Blocks (EGPRS)" },
	{ 0, NULL }
};

DEFUN(cfg_gsmtap_gsmtap_remote_host,
      cfg_gsmtap_gsmtap_remote_host_cmd,
      "remote-host [HOSTNAME]",
      "Enable GSMTAP Um logging\n"
      "Remote IP address or hostname ('localhost' if omitted)\n")
{
	osmo_talloc_replace_string(l23_ctx, &l23_cfg.gsmtap.remote_host,
				   argc > 0 ? argv[0] : "localhost");

	if (vty->type != VTY_FILE)
		vty_out(vty, "%% This command requires restart%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_no_gsmtap_remote_host,
      cfg_gsmtap_no_gsmtap_remote_host_cmd,
      "no remote-host",
      NO_STR "Disable GSMTAP Um logging\n")
{
	TALLOC_FREE(l23_cfg.gsmtap.remote_host);
	if (vty->type != VTY_FILE)
		vty_out(vty, "%% This command requires restart%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_gsmtap_local_host,
      cfg_gsmtap_gsmtap_local_host_cmd,
      "local-host " VTY_IPV46_CMD,
      "Set source for GSMTAP Um logging\n"
      "Local IPv4 address\n" "Local IPv6 address\n")
{
	osmo_talloc_replace_string(l23_ctx, &l23_cfg.gsmtap.local_host, argv[0]);

	if (vty->type != VTY_FILE)
		vty_out(vty, "%% This command requires restart%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_no_gsmtap_local_host,
      cfg_gsmtap_no_gsmtap_local_host_cmd,
      "no local-host",
      NO_STR "Disable explicit source for GSMTAP Um logging\n")
{
	TALLOC_FREE(l23_cfg.gsmtap.local_host);
	if (vty->type != VTY_FILE)
		vty_out(vty, "%% This command requires restart%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_gsmtap_lchan_all, cfg_gsmtap_gsmtap_lchan_all_cmd,
	"lchan (enable-all|disable-all)",
	"Enable/disable sending of UL/DL messages over GSMTAP\n"
	"Enable all kinds of messages (all LCHAN)\n"
	"Disable all kinds of messages (all LCHAN)\n")
{
	if (argv[0][0] == 'e') {
		l23_cfg.gsmtap.lchan_mask = UINT32_MAX;
		l23_cfg.gsmtap.lchan_acch_mask = UINT32_MAX;
		l23_cfg.gsmtap.lchan_acch = true;
	} else {
		l23_cfg.gsmtap.lchan_mask = 0x00;
		l23_cfg.gsmtap.lchan_acch_mask = 0x00;
		l23_cfg.gsmtap.lchan_acch = false;
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_gsmtap_lchan, cfg_gsmtap_gsmtap_lchan_cmd,
	"HIDDEN", "HIDDEN")
{
	unsigned int channel;

	if (osmo_str_startswith(argv[0], "sacch")) {
		if (strcmp(argv[0], "sacch") == 0) {
			l23_cfg.gsmtap.lchan_acch = true;
		} else {
			channel = get_string_value(gsmtap_gsm_channel_names, argv[0]);
			channel &= ~GSMTAP_CHANNEL_ACCH;
			l23_cfg.gsmtap.lchan_acch_mask |= (1 << channel);
		}
	} else {
		channel = get_string_value(gsmtap_gsm_channel_names, argv[0]);
		l23_cfg.gsmtap.lchan_mask |= (1 << channel);
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_no_gsmtap_lchan, cfg_gsmtap_no_gsmtap_lchan_cmd,
	"HIDDEN", "HIDDEN")
{
	unsigned int channel;

	if (osmo_str_startswith(argv[0], "sacch")) {
		if (strcmp(argv[0], "sacch") == 0) {
			l23_cfg.gsmtap.lchan_acch = false;
		} else {
			channel = get_string_value(gsmtap_gsm_channel_names, argv[0]);
			channel &= ~GSMTAP_CHANNEL_ACCH;
			l23_cfg.gsmtap.lchan_acch_mask &= ~(1 << channel);
		}
	} else {
		channel = get_string_value(gsmtap_gsm_channel_names, argv[0]);
		l23_cfg.gsmtap.lchan_mask &= ~(1 << channel);
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_gsmtap_categ_gprs_all, cfg_gsmtap_gsmtap_categ_gprs_all_cmd,
	"category gprs (enable-all|disable-all)",
	"Enable/disable sending of UL/DL messages over GSMTAP\n"
	"Enable all kinds of messages (all categories)\n"
	"Disable all kinds of messages (all categories)\n")
{

	if (strcmp(argv[0], "enable-all") == 0)
		l23_cfg.gsmtap.categ_gprs_mask = UINT32_MAX;
	else
		l23_cfg.gsmtap.categ_gprs_mask = 0x00;

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_gsmtap_categ_gprs, cfg_gsmtap_gsmtap_categ_gprs_cmd, "HIDDEN", "HIDDEN")
{
	int categ;

	categ = get_string_value(gsmtap_categ_gprs_names, argv[0]);
	if (categ < 0)
		return CMD_WARNING;

	l23_cfg.gsmtap.categ_gprs_mask |= (1 << categ);

	return CMD_SUCCESS;
}

DEFUN(cfg_gsmtap_no_gsmtap_categ_gprs, cfg_gsmtap_no_gsmtap_categ_gprs_cmd, "HIDDEN", "HIDDEN")
{
	int categ;

	categ = get_string_value(gsmtap_categ_gprs_names, argv[0]);
	if (categ < 0)
		return CMD_WARNING;

	l23_cfg.gsmtap.categ_gprs_mask &= ~(1 << categ);

	return CMD_SUCCESS;
}


gDEFUN(l23_show_ms, l23_show_ms_cmd, "show ms [MS_NAME]",
	SHOW_STR "Display available MS entities\n")
{
	struct osmocom_ms *ms;

	if (argc) {
		llist_for_each_entry(ms, &ms_list, entity) {
			if (!strcmp(ms->name, argv[0])) {
				l23_ms_dump(ms, vty);
				return CMD_SUCCESS;
			}
		}
		vty_out(vty, "MS name '%s' does not exist.%s", argv[0],
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	llist_for_each_entry(ms, &ms_list, entity) {
		l23_ms_dump(ms, vty);
		vty_out(vty, "%s", VTY_NEWLINE);
	}

	return CMD_SUCCESS;
}

static int _sim_testcard_cmd(struct vty *vty, int argc, const char *argv[],
	int attached)
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;
	int rc;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (ms->subscr.sim_valid) {
		vty_out(vty, "SIM already attached, remove first!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	set = &ms->settings;
	set->sim_type = GSM_SIM_TYPE_TEST;

	if (argc == 2) {
		vty_out(vty, "Give MNC together with MCC%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (argc >= 3) {
		struct osmo_plmn_id plmn;
		if (osmo_mcc_from_str(argv[1], &plmn.mcc) < 0) {
			vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		if (osmo_mnc_from_str(argv[2], &plmn.mnc, &plmn.mnc_3_digits) < 0) {
			vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		memcpy(&set->test_sim.rplmn, &plmn, sizeof(plmn));
		set->test_sim.rplmn_valid = true;
	} else {
		set->test_sim.rplmn_valid = false;
	}

	if (argc >= 4)
		set->test_sim.lac = strtoul(argv[3], NULL, 16);

	if (argc >= 5)
		set->test_sim.tmsi = strtoul(argv[4], NULL, 16);

	set->test_sim.imsi_attached = attached;

	rc = gsm_subscr_insert(ms);
	if (rc < 0) {
		vty_out(vty, "Attach test SIM card failed: %d%s", rc, VTY_NEWLINE);
		return CMD_WARNING;
	}

	return CMD_SUCCESS;
}

DEFUN(sim_testcard, sim_testcard_cmd,
	"sim testcard MS_NAME [MCC] [MNC] [LAC] [TMSI]",
	"SIM actions\nAttach built in test SIM\nName of MS (see \"show ms\")\n"
	"Optionally set mobile Country Code of RPLMN\n"
	"Optionally set mobile Network Code of RPLMN\n"
	"Optionally set location area code of RPLMN\n"
	"Optionally set current assigned TMSI")
{
	return _sim_testcard_cmd(vty, argc, argv, 0);
}

DEFUN(sim_testcard_att, sim_testcard_att_cmd,
	"sim testcard MS_NAME MCC MNC LAC TMSI attached",
	"SIM actions\nAttach built in test SIM\nName of MS (see \"show ms\")\n"
	"Set mobile Country Code of RPLMN\nSet mobile Network Code of RPLMN\n"
	"Set location area code\nSet current assigned TMSI\n"
	"Indicate to MM that card is already attached")
{
	return _sim_testcard_cmd(vty, argc, argv, 1);
}

DEFUN(sim_sap, sim_sap_cmd, "sim sap MS_NAME",
	"SIM actions\nAttach SIM over SAP interface\n"
	"Name of MS (see \"show ms\")\n")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (ms->subscr.sim_valid) {
		vty_out(vty, "SIM already attached, remove first!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	set = &ms->settings;
	set->sim_type = GSM_SIM_TYPE_SAP;
	if (gsm_subscr_insert(ms) != 0)
		return CMD_WARNING;

	return CMD_SUCCESS;
}

DEFUN(sim_reader, sim_reader_cmd, "sim reader MS_NAME",
	"SIM actions\nAttach SIM from reader\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (ms->subscr.sim_valid) {
		vty_out(vty, "SIM already attached, remove first!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	set = &ms->settings;
	set->sim_type = GSM_SIM_TYPE_L1PHY;
	gsm_subscr_insert(ms);

	return CMD_SUCCESS;
}

DEFUN(sim_remove, sim_remove_cmd, "sim remove MS_NAME",
	"SIM actions\nDetach SIM card\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (!ms->subscr.sim_valid) {
		vty_out(vty, "No SIM attached!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_remove(ms);
	return CMD_SUCCESS;
}

DEFUN(sim_pin, sim_pin_cmd, "sim pin MS_NAME PIN",
	"SIM actions\nEnter PIN for SIM card\nName of MS (see \"show ms\")\n"
	"PIN number")
{
	struct osmocom_ms *ms;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (!ms->subscr.sim_pin_required) {
		vty_out(vty, "No PIN is required at this time!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], "", 0);

	return CMD_SUCCESS;
}

DEFUN(sim_disable_pin, sim_disable_pin_cmd, "sim disable-pin MS_NAME PIN",
	"SIM actions\nDisable PIN of SIM card\nName of MS (see \"show ms\")\n"
	"PIN number")
{
	struct osmocom_ms *ms;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], "", -1);

	return CMD_SUCCESS;
}

DEFUN(sim_enable_pin, sim_enable_pin_cmd, "sim enable-pin MS_NAME PIN",
	"SIM actions\nEnable PIN of SIM card\nName of MS (see \"show ms\")\n"
	"PIN number")
{
	struct osmocom_ms *ms;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], "", 1);

	return CMD_SUCCESS;
}

DEFUN(sim_change_pin, sim_change_pin_cmd, "sim change-pin MS_NAME OLD NEW",
	"SIM actions\nChange PIN of SIM card\nName of MS (see \"show ms\")\n"
	"Old PIN number\nNew PIN number")
{
	struct osmocom_ms *ms;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "Old PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (strlen(argv[2]) < 4 || strlen(argv[2]) > 8) {
		vty_out(vty, "New PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], (char *)argv[2], 2);

	return CMD_SUCCESS;
}

DEFUN(sim_unblock_pin, sim_unblock_pin_cmd, "sim unblock-pin MS_NAME PUC NEW",
	"SIM actions\nChange PIN of SIM card\nName of MS (see \"show ms\")\n"
	"Personal Unblock Key\nNew PIN number")
{
	struct osmocom_ms *ms;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) != 8) {
		vty_out(vty, "PUC must be 8 digits!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (strlen(argv[2]) < 4 || strlen(argv[2]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], (char *)argv[2], 99);

	return CMD_SUCCESS;
}

DEFUN(sim_lai, sim_lai_cmd, "sim lai MS_NAME MCC MNC LAC",
	"SIM actions\nChange LAI of SIM card\nName of MS (see \"show ms\")\n"
	"Mobile Country Code\nMobile Network Code\nLocation Area Code "
	" (use 0000 to remove LAI)")
{
	struct osmocom_ms *ms;
	struct osmo_plmn_id plmn;
	uint16_t lac;

	ms = l23_vty_get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (osmo_mcc_from_str(argv[1], &plmn.mcc) < 0) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (osmo_mnc_from_str(argv[2], &plmn.mnc, &plmn.mnc_3_digits) < 0) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	lac = strtoul(argv[3], NULL, 0);

	memcpy(&ms->subscr.lai.plmn, &plmn, sizeof(plmn));
	ms->subscr.lai.lac = lac;
	ms->subscr.tmsi = GSM_RESERVED_TMSI;

	gsm_subscr_write_loci(ms);

	return CMD_SUCCESS;
}

/* per MS config */
gDEFUN(l23_cfg_ms, l23_cfg_ms_cmd, "ms MS_NAME",
	"Select a mobile station to configure\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, argv[0])) {
			vty->index = ms;
			vty->node = MS_NODE;
			return CMD_SUCCESS;
		}
	}

	vty_out(vty, "MS name '%s' does not exits%s", argv[0],
		VTY_NEWLINE);
	return CMD_WARNING;
}

DEFUN(cfg_ms_layer2, cfg_ms_layer2_cmd, "layer2-socket PATH",
	"Define socket path to connect between layer 2 and layer 1\n"
	"Unix socket, default '" L2_DEFAULT_SOCKET_PATH "'")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	OSMO_STRLCPY_ARRAY(set->layer2_socket_path, argv[0]);

	l23_vty_restart_required_warn(vty, ms);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_imei, cfg_ms_imei_cmd, "imei IMEI [SV]",
	"Set IMEI (enter without control digit)\n15 Digits IMEI\n"
	"Software version digit")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	char *error, *sv = "0";

	if (argc >= 2)
		sv = (char *)argv[1];

	error = gsm_check_imei(argv[0], sv);
	if (error) {
		vty_out(vty, "%s%s", error, VTY_NEWLINE);
		return CMD_WARNING;
	}

	OSMO_STRLCPY_ARRAY(set->imei, argv[0]);
	OSMO_STRLCPY_ARRAY(set->imeisv, argv[0]);
	osmo_strlcpy(set->imeisv + GSM23003_IMEI_NUM_DIGITS, sv,
		     sizeof(set->imeisv) - GSM23003_IMEI_NUM_DIGITS);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_imei_fixed, cfg_ms_imei_fixed_cmd, "imei-fixed",
	"Use fixed IMEI on every power on")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->imei_random = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_imei_random, cfg_ms_imei_random_cmd, "imei-random <0-15>",
	"Use random IMEI on every power on\n"
	"Number of trailing digits to randomize")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->imei_random = atoi(argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sim, cfg_ms_sim_cmd, "sim (none|reader|test|sap)",
	"Set SIM card to attach when powering on\nAttach no SIM\n"
	"Attach SIM from reader\nAttach build in test SIM\n"
	"Attach SIM over SAP interface")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	switch (argv[0][0]) {
	case 'n':
		set->sim_type = GSM_SIM_TYPE_NONE;
		break;
	case 'r':
		set->sim_type = GSM_SIM_TYPE_L1PHY;
		break;
	case 't':
		set->sim_type = GSM_SIM_TYPE_TEST;
		break;
	case 's':
		set->sim_type = GSM_SIM_TYPE_SAP;
		break;
	default:
		vty_out(vty, "unknown SIM type%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_shutdown, cfg_ms_no_shutdown_cmd, "no shutdown",
	NO_STR "Activate and run MS")
{
	struct osmocom_ms *ms = vty->index;

	struct osmobb_l23_vty_sig_data data;
	memset(&data, 0, sizeof(data));

	data.vty = vty;
	data.ms_start.ms = ms;
	data.ms_start.rc = CMD_SUCCESS;
	osmo_signal_dispatch(SS_L23_VTY, S_L23_VTY_MS_START, &data);

	return data.ms_start.rc;
}

DEFUN(cfg_ms_shutdown, cfg_ms_shutdown_cmd, "shutdown",
	"Shut down and deactivate MS")
{
	struct osmocom_ms *ms = vty->index;

	struct osmobb_l23_vty_sig_data data;
	memset(&data, 0, sizeof(data));

	data.vty = vty;
	data.ms_stop.ms = ms;
	data.ms_stop.force = false;
	data.ms_stop.rc = CMD_SUCCESS;
	osmo_signal_dispatch(SS_L23_VTY, S_L23_VTY_MS_STOP, &data);

	return data.ms_stop.rc;
}

DEFUN(cfg_ms_shutdown_force, cfg_ms_shutdown_force_cmd, "shutdown force",
	"Shut down and deactivate MS\nDo not perform IMSI detach")
{
	struct osmocom_ms *ms = vty->index;

	struct osmobb_l23_vty_sig_data data;
	memset(&data, 0, sizeof(data));

	data.vty = vty;
	data.ms_stop.ms = ms;
	data.ms_stop.force = true;
	data.ms_stop.rc = CMD_SUCCESS;
	osmo_signal_dispatch(SS_L23_VTY, S_L23_VTY_MS_STOP, &data);

	return data.ms_stop.rc;
}

/* per testsim config */
DEFUN(cfg_ms_testsim, cfg_ms_testsim_cmd, "test-sim",
	"Configure test SIM emulation")
{
	vty->node = TESTSIM_NODE;

	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_imsi, cfg_testsim_imsi_cmd, "imsi IMSI",
	"Set IMSI on test card\n15 digits IMSI")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	if (!osmo_imsi_str_valid(argv[0])) {
		vty_out(vty, "Wrong IMSI format%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	OSMO_STRLCPY_ARRAY(set->test_sim.imsi, argv[0]);

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

#define HEX_STR "\nByte as two digits hexadecimal"
DEFUN(cfg_testsim_ki_xor, cfg_testsim_ki_xor_cmd, "ki xor HEX HEX HEX HEX HEX HEX "
	"HEX HEX HEX HEX HEX HEX",
	"Set Key (Ki) on test card\nUse XOR algorithm" HEX_STR HEX_STR HEX_STR
	HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	uint8_t ki[12];
	const char *p;
	int i;

	for (i = 0; i < 12; i++) {
		p = argv[i];
		if (!strncmp(p, "0x", 2))
			p += 2;
		if (strlen(p) != 2) {
			vty_out(vty, "Expecting two digits hex value (with or "
				"without 0x in front)%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		ki[i] = strtoul(p, NULL, 16);
	}

	set->test_sim.ki_type = OSMO_AUTH_ALG_XOR;
	memcpy(set->test_sim.ki, ki, 12);
	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_ki_comp128, cfg_testsim_ki_comp128_cmd, "ki comp128 HEX HEX HEX "
	"HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX",
	"Set Key (Ki) on test card\nUse XOR algorithm" HEX_STR HEX_STR HEX_STR
	HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR
	HEX_STR HEX_STR HEX_STR HEX_STR)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	uint8_t ki[16];
	const char *p;
	int i;

	for (i = 0; i < 16; i++) {
		p = argv[i];
		if (!strncmp(p, "0x", 2))
			p += 2;
		if (strlen(p) != 2) {
			vty_out(vty, "Expecting two digits hex value (with or "
				"without 0x in front)%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		ki[i] = strtoul(p, NULL, 16);
	}

	set->test_sim.ki_type = OSMO_AUTH_ALG_COMP128v1;
	memcpy(set->test_sim.ki, ki, 16);
	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_barr, cfg_testsim_barr_cmd, "barred-access",
	"Allow access to barred cells")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_sim.barr = true;

	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_no_barr, cfg_testsim_no_barr_cmd, "no barred-access",
	NO_STR "Deny access to barred cells")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_sim.barr = false;

	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_no_rplmn, cfg_testsim_no_rplmn_cmd, "no rplmn",
	NO_STR "Unset Registered PLMN")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_sim.rplmn_valid = false;
	set->test_sim.rplmn.mcc = 1;
	set->test_sim.rplmn.mnc = 1;
	set->test_sim.rplmn.mnc_3_digits = false;
	set->test_sim.lac = 0x0000;
	set->test_sim.tmsi = GSM_RESERVED_TMSI;

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

static int _testsim_rplmn_cmd(struct vty *vty, int argc, const char *argv[], bool attached)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct osmo_plmn_id plmn;

	if (osmo_mcc_from_str(argv[0], &plmn.mcc) < 0) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (osmo_mnc_from_str(argv[1], &plmn.mnc, &plmn.mnc_3_digits) < 0) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	set->test_sim.rplmn_valid = true;
	memcpy(&set->test_sim.rplmn, &plmn, sizeof(plmn));

	if (argc >= 3)
		set->test_sim.lac = strtoul(argv[2], NULL, 16);
	else
		set->test_sim.lac = 0xfffe;

	if (argc >= 4)
		set->test_sim.tmsi = strtoul(argv[3], NULL, 16);
	else
		set->test_sim.tmsi = GSM_RESERVED_TMSI;

	set->test_sim.imsi_attached = attached;

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_rplmn, cfg_testsim_rplmn_cmd,
	"rplmn MCC MNC [LAC] [TMSI]",
	"Set Registered PLMN\nMobile Country Code\nMobile Network Code\n"
	"Optionally set location area code\n"
	"Optionally set current assigned TMSI")
{
	return _testsim_rplmn_cmd(vty, argc, argv, false);
}

DEFUN(cfg_testsim_rplmn_att, cfg_testsim_rplmn_att_cmd,
	"rplmn MCC MNC LAC TMSI attached",
	"Set Registered PLMN\nMobile Country Code\nMobile Network Code\n"
	"Set location area code\nSet current assigned TMSI\n"
	"Indicate to MM that card is already attached")
{
	return _testsim_rplmn_cmd(vty, argc, argv, true);
}

DEFUN(cfg_testsim_hplmn, cfg_testsim_hplmn_cmd, "hplmn-search (everywhere|foreign-country)",
	"Set Home PLMN search mode\n"
	"Search for HPLMN when on any other network\n"
	"Search for HPLMN when in a different country")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	switch (argv[0][0]) {
	case 'e':
		set->test_sim.always_search_hplmn = true;
		break;
	case 'f':
		set->test_sim.always_search_hplmn = false;
		break;
	}

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

static int _testsim_locigprs_cmd(struct vty *vty, int argc, const char *argv[], bool attached)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct osmo_plmn_id plmn;

	if (osmo_mcc_from_str(argv[0], &plmn.mcc) < 0) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (osmo_mnc_from_str(argv[1], &plmn.mnc, &plmn.mnc_3_digits) < 0) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	set->test_sim.locigprs.valid = true;
	set->test_sim.locigprs.rai.mcc = plmn.mcc;
	set->test_sim.locigprs.rai.mnc = plmn.mnc;
	set->test_sim.locigprs.rai.mnc_3_digits = plmn.mnc_3_digits;

	if (argc >= 3)
		set->test_sim.locigprs.rai.lac = strtoul(argv[2], NULL, 16);
	else
		set->test_sim.locigprs.rai.lac = 0xfffe;

	if (argc >= 4)
		set->test_sim.locigprs.rai.rac = strtoul(argv[3], NULL, 16);
	else
		set->test_sim.locigprs.rai.rac = 0xff;

	if (argc >= 5)
		set->test_sim.locigprs.ptmsi = strtoul(argv[4], NULL, 16);
	else
		set->test_sim.locigprs.ptmsi = GSM_RESERVED_TMSI;

	if (argc >= 6)
		set->test_sim.locigprs.ptmsi_sig = strtoul(argv[5], NULL, 16);
	else
		set->test_sim.locigprs.ptmsi_sig = GSM_RESERVED_TMSI;

	set->test_sim.locigprs.imsi_attached = attached;

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_no_locigprs, cfg_testsim_no_locigprs_cmd, "no locigprs",
	NO_STR "Unset EF LOCIgprs\n")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_sim.locigprs.valid = false;
	set->test_sim.locigprs.ptmsi = GSM_RESERVED_TMSI;
	set->test_sim.locigprs.ptmsi_sig = GSM_RESERVED_TMSI;
	set->test_sim.locigprs.rai.mcc = 1;
	set->test_sim.locigprs.rai.mnc = 1;
	set->test_sim.locigprs.rai.mnc_3_digits = false;
	set->test_sim.locigprs.rai.lac = 0x0000;
	set->test_sim.locigprs.rai.rac = 0x0000;

	l23_vty_restart_required_warn(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_testsim_locigprs, cfg_testsim_locigprs_cmd,
	"locigprs MCC MNC [LAC] [RAC] [PTMSI] [PTMSISIG]",
	"Set EF LOCIgprs\nMobile Country Code\nMobile Network Code\n"
	"Optionally set location area code\n"
	"Optionally set routing area code\n"
	"Optionally set current assigned P-TMSI\n"
	"Optionally set current assigned P-TMSI signature\n")
{
	return _testsim_locigprs_cmd(vty, argc, argv, false);
}

DEFUN(cfg_testsim_locigprs_att, cfg_testsim_locigprs_att_cmd,
	"locigprs MCC MNC LAC RAC PTMSI PTMSISIG attached",
	"Set EF LOCIgprs\nMobile Country Code\nMobile Network Code\n"
	"Set location area code\n"
	"Set routing area code\n"
	"Set current assigned P-TMSI\n"
	"Set current assigned P-TMSI signature\n"
	"Indicate to MM that card is already attached\n")
{
	return _testsim_locigprs_cmd(vty, argc, argv, true);
}

static int l23_vty_config_write_gsmtap_node(struct vty *vty)
{
	const char *chan_buf;
	unsigned int i;

	vty_out(vty, "gsmtap%s", VTY_NEWLINE);

	if (l23_cfg.gsmtap.remote_host)
		vty_out(vty, " remote-host %s%s", l23_cfg.gsmtap.remote_host, VTY_NEWLINE);
	else
		vty_out(vty, " no remote-host%s", VTY_NEWLINE);

	if (l23_cfg.gsmtap.local_host)
		vty_out(vty, " local-host %s%s", l23_cfg.gsmtap.local_host, VTY_NEWLINE);
	else
		vty_out(vty, " no local-host%s", VTY_NEWLINE);

	if (l23_cfg.gsmtap.lchan_acch)
		vty_out(vty, " lchan sacch%s", VTY_NEWLINE);

	for (i = 0; i < sizeof(uint32_t) * 8; i++) {
		if (l23_cfg.gsmtap.lchan_acch_mask & ((uint32_t) 1 << i)) {
			chan_buf = get_value_string_or_null(gsmtap_gsm_channel_names, GSMTAP_CHANNEL_ACCH | i);
			if (chan_buf == NULL)
				continue;
			chan_buf = osmo_str_tolower(chan_buf);
			vty_out(vty, " lchan %s%s", chan_buf, VTY_NEWLINE);
		}
	}

	for (i = 0; i < sizeof(uint32_t) * 8; i++) {
		if (l23_cfg.gsmtap.lchan_mask & ((uint32_t) 1 << i)) {
			chan_buf = get_value_string_or_null(gsmtap_gsm_channel_names, i);
			if (chan_buf == NULL)
				continue;
			chan_buf = osmo_str_tolower(chan_buf);
			vty_out(vty, " lchan %s%s", chan_buf, VTY_NEWLINE);
		}
	}

	for (i = 0; i < 32; i++) {
		if (l23_cfg.gsmtap.categ_gprs_mask & ((uint32_t)1 << i)) {
			const char *category_buf;
			if (!(category_buf = get_value_string_or_null(gsmtap_categ_gprs_names, i)))
				continue;
			vty_out(vty, " category gprs %s%s", category_buf, VTY_NEWLINE);
		}
	}

	return CMD_SUCCESS;
}

static int l23_vty_config_write_testsim_node(struct vty *vty, const struct osmocom_ms *ms, const char *prefix)
{
	const struct gsm_settings *set = &ms->settings;
	vty_out(vty, "%stest-sim%s", prefix, VTY_NEWLINE);
	vty_out(vty, "%s imsi %s%s", prefix, set->test_sim.imsi, VTY_NEWLINE);
	switch (set->test_sim.ki_type) {
	case OSMO_AUTH_ALG_XOR:
		vty_out(vty, "%s ki xor %s%s",
			prefix, osmo_hexdump(set->test_sim.ki, 12), VTY_NEWLINE);
		break;
	case OSMO_AUTH_ALG_COMP128v1:
		vty_out(vty, "%s ki comp128 %s%s",
			prefix, osmo_hexdump(set->test_sim.ki, 16), VTY_NEWLINE);
		break;
	}
	if (!l23_vty_hide_default || set->test_sim.barr)
		vty_out(vty, "%s %sbarred-access%s", prefix,
			(set->test_sim.barr) ? "" : "no ", VTY_NEWLINE);
	if (set->test_sim.rplmn_valid) {
		vty_out(vty, "%s rplmn %s %s", prefix,
			osmo_mcc_name(set->test_sim.rplmn.mcc),
			osmo_mnc_name(set->test_sim.rplmn.mnc, set->test_sim.rplmn.mnc_3_digits));
		if (set->test_sim.lac > 0x0000 && set->test_sim.lac < 0xfffe) {
			vty_out(vty, " 0x%04x", set->test_sim.lac);
			if (set->test_sim.tmsi != GSM_RESERVED_TMSI) {
				vty_out(vty, " 0x%08x", set->test_sim.tmsi);
				if (set->test_sim.imsi_attached)
					vty_out(vty, " attached");
			}
		}
		vty_out(vty, "%s", VTY_NEWLINE);
	} else
		if (!l23_vty_hide_default)
			vty_out(vty, "%s no rplmn%s", prefix, VTY_NEWLINE);

	if (!l23_vty_hide_default || set->test_sim.always_search_hplmn)
		vty_out(vty, "%s hplmn-search %s%s", prefix,
			set->test_sim.always_search_hplmn ? "everywhere" : "foreign-country",
			VTY_NEWLINE);

	if (set->test_sim.locigprs.valid) {
		vty_out(vty, "%s locigprs %s %s", prefix,
			osmo_mcc_name(set->test_sim.locigprs.rai.mcc),
			osmo_mnc_name(set->test_sim.locigprs.rai.mnc,
				      set->test_sim.locigprs.rai.mnc_3_digits));
		if (set->test_sim.locigprs.rai.lac > 0x0000 && set->test_sim.locigprs.rai.lac < 0xfffe) {
			vty_out(vty, " 0x%04x", set->test_sim.locigprs.rai.lac);
			if (set->test_sim.locigprs.rai.rac < 0xff) {
				vty_out(vty, " 0x%02x", set->test_sim.locigprs.rai.rac);
				if (set->test_sim.locigprs.ptmsi != GSM_RESERVED_TMSI) {
					vty_out(vty, " 0x%08x 0x%06x",
						set->test_sim.locigprs.ptmsi,
						set->test_sim.locigprs.ptmsi_sig);
					if (set->test_sim.locigprs.imsi_attached)
						vty_out(vty, " attached");
				}
			}
		}
		vty_out(vty, "%s", VTY_NEWLINE);
	} else
		if (!l23_vty_hide_default)
			vty_out(vty, "%s no locigprs%s", prefix, VTY_NEWLINE);
	return CMD_SUCCESS;
}

void l23_vty_config_write_ms_node(struct vty *vty, const struct osmocom_ms *ms, const char *prefix)
{
	size_t prefix_len = strlen(prefix);
	char *prefix_content = alloca(prefix_len + 1 + 1);

	memcpy(prefix_content, prefix, prefix_len);
	prefix_content[prefix_len] = ' ';
	prefix_content[prefix_len + 1] = '\0';

	vty_out(vty, "%sms %s%s", prefix, ms->name, VTY_NEWLINE);
	l23_vty_config_write_ms_node_contents(vty, ms, prefix_content);
	l23_vty_config_write_ms_node_contents_final(vty, ms, prefix_content);
}

/* placeholder for shared VTY commands */
void l23_vty_config_write_ms_node_contents(struct vty *vty, const struct osmocom_ms *ms, const char *prefix)
{
	const struct gsm_settings *set = &ms->settings;

	vty_out(vty, "%slayer2-socket %s%s", prefix, set->layer2_socket_path,
		VTY_NEWLINE);

	vty_out(vty, "%simei %s %s%s", prefix, set->imei,
		set->imeisv + strlen(set->imei), VTY_NEWLINE);
	if (set->imei_random)
		vty_out(vty, "%simei-random %d%s", prefix, set->imei_random, VTY_NEWLINE);
	else if (!l23_vty_hide_default)
		vty_out(vty, "%simei-fixed%s", prefix, VTY_NEWLINE);

	switch (set->sim_type) {
	case GSM_SIM_TYPE_NONE:
		vty_out(vty, "%ssim none%s", prefix,  VTY_NEWLINE);
		break;
	case GSM_SIM_TYPE_L1PHY:
		vty_out(vty, "%ssim reader%s", prefix,  VTY_NEWLINE);
		break;
	case GSM_SIM_TYPE_TEST:
		vty_out(vty, "%ssim test%s", prefix,  VTY_NEWLINE);
		break;
	case GSM_SIM_TYPE_SAP:
		vty_out(vty, "%ssim sap%s", prefix,  VTY_NEWLINE);
		break;
	default:
		OSMO_ASSERT(0);
	}

	l23_vty_config_write_testsim_node(vty, ms, prefix);
}

/* placeholder for shared VTY commands. Must be put at the end of the node: */
void l23_vty_config_write_ms_node_contents_final(struct vty *vty, const struct osmocom_ms *ms, const char *prefix)
{
	/* no shutdown must be written to config, because shutdown is default */
	vty_out(vty, "%s%sshutdown%s", prefix, (ms->shutdown != MS_SHUTDOWN_NONE) ? "" : "no ",
		VTY_NEWLINE);
	vty_out(vty, "!%s", VTY_NEWLINE);
}

static void l23_vty_init_gsmtap(void)
{
	cfg_gsmtap_gsmtap_lchan_cmd.string = vty_cmd_string_from_valstr(l23_ctx, gsmtap_gsm_channel_names,
						"lchan (sacch|",
						"|", ")", VTY_DO_LOWER);
	cfg_gsmtap_gsmtap_lchan_cmd.doc = vty_cmd_string_from_valstr(l23_ctx, gsmtap_gsm_channel_names,
						"Enable sending of UL/DL messages over GSMTAP\n" "SACCH\n",
						"\n", "", 0);

	cfg_gsmtap_no_gsmtap_lchan_cmd.string = vty_cmd_string_from_valstr(l23_ctx, gsmtap_gsm_channel_names,
						"no lchan (sacch|",
						"|", ")", VTY_DO_LOWER);
	cfg_gsmtap_no_gsmtap_lchan_cmd.doc = vty_cmd_string_from_valstr(l23_ctx, gsmtap_gsm_channel_names,
						NO_STR "Disable sending of UL/DL messages over GSMTAP\n" "SACCH\n",
						"\n", "", 0);


	cfg_gsmtap_gsmtap_categ_gprs_cmd.string = vty_cmd_string_from_valstr(l23_ctx, gsmtap_categ_gprs_names,
						"category gprs (",
						"|", ")", VTY_DO_LOWER);
	cfg_gsmtap_gsmtap_categ_gprs_cmd.doc = vty_cmd_string_from_valstr(l23_ctx, gsmtap_categ_gprs_help,
						"GSMTAP Category\n" "GPRS\n",
						"\n", "", 0);
	cfg_gsmtap_no_gsmtap_categ_gprs_cmd.string = vty_cmd_string_from_valstr(l23_ctx, gsmtap_categ_gprs_names,
						"no category gprs (",
						"|", ")", VTY_DO_LOWER);
	cfg_gsmtap_no_gsmtap_categ_gprs_cmd.doc = vty_cmd_string_from_valstr(l23_ctx, gsmtap_categ_gprs_help,
						NO_STR "GSMTAP Category\n" "GPRS\n",
						"\n", "", 0);

	install_element(CONFIG_NODE, &l23_cfg_gsmtap_cmd);

	install_node(&gsmtap_node, l23_vty_config_write_gsmtap_node);
	install_element(GSMTAP_NODE, &cfg_gsmtap_gsmtap_remote_host_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_no_gsmtap_remote_host_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_gsmtap_local_host_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_no_gsmtap_local_host_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_gsmtap_lchan_all_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_gsmtap_lchan_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_no_gsmtap_lchan_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_gsmtap_categ_gprs_all_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_gsmtap_categ_gprs_cmd);
	install_element(GSMTAP_NODE, &cfg_gsmtap_no_gsmtap_categ_gprs_cmd);
}

int l23_vty_init(int (*config_write_ms_node_cb)(struct vty *), osmo_signal_cbfn *l23_vty_signal_cb)
{
	int rc = 0;

	if (l23_app_info.opt_supported & L23_OPT_TAP)
		l23_vty_init_gsmtap();

	if (l23_app_info.opt_supported & L23_OPT_VTY)
		osmo_stats_vty_add_cmds();

	install_element_ve(&show_subscr_cmd);
	install_element_ve(&show_support_cmd);

	install_element(ENABLE_NODE, &sim_testcard_cmd);
	install_element(ENABLE_NODE, &sim_testcard_att_cmd);
	install_element(ENABLE_NODE, &sim_sap_cmd);
	install_element(ENABLE_NODE, &sim_reader_cmd);
	install_element(ENABLE_NODE, &sim_remove_cmd);
	install_element(ENABLE_NODE, &sim_pin_cmd);
	install_element(ENABLE_NODE, &sim_disable_pin_cmd);
	install_element(ENABLE_NODE, &sim_enable_pin_cmd);
	install_element(ENABLE_NODE, &sim_change_pin_cmd);
	install_element(ENABLE_NODE, &sim_unblock_pin_cmd);
	install_element(ENABLE_NODE, &sim_lai_cmd);

	install_element(CONFIG_NODE, &cfg_hide_default_cmd);
	install_element(CONFIG_NODE, &cfg_no_hide_default_cmd);

	install_node(&ms_node, config_write_ms_node_cb);
	install_element(MS_NODE, &cfg_ms_layer2_cmd);
	install_element(MS_NODE, &cfg_ms_imei_cmd);
	install_element(MS_NODE, &cfg_ms_imei_fixed_cmd);
	install_element(MS_NODE, &cfg_ms_imei_random_cmd);
	install_element(MS_NODE, &cfg_ms_sim_cmd);
	install_element(MS_NODE, &cfg_ms_testsim_cmd);
	install_node(&testsim_node, NULL);
	install_element(TESTSIM_NODE, &cfg_testsim_imsi_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_ki_xor_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_ki_comp128_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_barr_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_no_barr_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_no_rplmn_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_rplmn_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_rplmn_att_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_hplmn_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_no_locigprs_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_locigprs_cmd);
	install_element(TESTSIM_NODE, &cfg_testsim_locigprs_att_cmd);
	install_element(MS_NODE, &cfg_ms_shutdown_cmd);
	install_element(MS_NODE, &cfg_ms_shutdown_force_cmd);
	install_element(MS_NODE, &cfg_ms_no_shutdown_cmd);

	/* Register the talloc context introspection command */
	osmo_talloc_vty_add_cmds();
	osmo_cpu_sched_vty_init(l23_ctx);
	if (l23_vty_signal_cb)
		rc = osmo_signal_register_handler(SS_L23_VTY, l23_vty_signal_cb, NULL);
	return rc;
}

