/*
 * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * All Rights Reserved.
 *
 * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <osmocom/core/application.h>
#include <osmocom/core/signal.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/fsm.h>
#include <osmocom/core/stats.h>
#include <osmocom/core/msgb.h>
#include <osmocom/vty/logging.h>
#include <osmocom/vty/command.h>
#include <osmocom/vty/misc.h>
#include <osmocom/vty/cpu_sched_vty.h>
#include <osmocom/vty/telnet_interface.h>
#include <osmocom/vty/ports.h>
#include <osmocom/vty/tdef_vty.h>
#include <osmocom/ctrl/control_if.h>
#include <osmocom/ctrl/control_vty.h>
#include <osmocom/ctrl/ports.h>

#include <osmocom/pfcp/pfcp_endpoint.h>

#include <osmocom/upf/upf.h>
#include <osmocom/upf/up_endpoint.h>
#include <osmocom/upf/upf_gtp.h>
#include <osmocom/upf/upf_nft.h>

#define _GNU_SOURCE
#include <getopt.h>

/* build switches from the configure script */
#include "config.h"

#include <signal.h>
#include <stdio.h>
#include <string.h>

extern void *tall_vty_ctx;

void *tall_upf_ctx = NULL;
static int quit = 0;

static struct {
	const char *config_file;
	int daemonize;
	enum vty_ref_gen_mode vty_ref_gen_mode;
} upf_cmdline_config = {
	.config_file = "osmo-upf.cfg",
	.vty_ref_gen_mode = VTY_REF_GEN_MODE_DEFAULT,
};

static void print_usage()
{
	printf("Usage: osmo-upf\n");
}

static void print_help()
{
	const struct value_string *vty_ref_gen_mode_name;

	printf("Some useful options:\n");
	printf("  -h --help                  This text.\n");
	printf("  -D --daemonize             Fork the process into a background daemon.\n");
	printf("  -c --config-file filename  The config file to use.\n");
	printf("  -V --version               Print the version of OsmoMSC.\n");

	printf("\nVTY reference generation:\n");
	printf("     --vty-ref-xml           Generate the VTY reference XML output and exit.\n");
	printf("     --vty-ref-mode MODE     Mode for --vty-ref-xml:\n");
	/* List all VTY ref gen modes */
	for (vty_ref_gen_mode_name = vty_ref_gen_mode_names; vty_ref_gen_mode_name->str; vty_ref_gen_mode_name++)
		printf("                    %s: %s\n",
		       vty_ref_gen_mode_name->str,
		       get_value_string(vty_ref_gen_mode_desc, vty_ref_gen_mode_name->value));
}

static void handle_long_options(const char *prog_name, const int long_option)
{
	switch (long_option) {
	case 1:
		upf_cmdline_config.vty_ref_gen_mode = get_string_value(vty_ref_gen_mode_names, optarg);
		if (upf_cmdline_config.vty_ref_gen_mode < 0) {
			fprintf(stderr, "%s: Unknown VTY reference generation mode: '%s'\n", prog_name, optarg);
			exit(2);
		}
		break;
	case 2:
		fprintf(stderr, "Generating the VTY reference in mode '%s' (%s)\n",
			get_value_string(vty_ref_gen_mode_names, upf_cmdline_config.vty_ref_gen_mode),
			get_value_string(vty_ref_gen_mode_desc, upf_cmdline_config.vty_ref_gen_mode));
		vty_dump_xml_ref_mode(stdout, upf_cmdline_config.vty_ref_gen_mode);
		exit(0);
	default:
		fprintf(stderr, "%s: error parsing cmdline options\n", prog_name);
		exit(2);
	}
}

static void handle_options(int argc, char **argv)
{
	while (1) {
		int option_index = 0, c;
		static int long_option = 0;
		static struct option long_options[] = {
			{"help", 0, 0, 'h'},
			{"daemonize", 0, 0, 'D'},
			{"config-file", 1, 0, 'c'},
			{"version", 0, 0, 'V' },
			{"vty-ref-mode", 1, &long_option, 1},
			{"vty-ref-xml", 0, &long_option, 2},
			{0, 0, 0, 0}
		};

		c = getopt_long(argc, argv, "hDc:V", long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 'h':
			print_usage();
			print_help();
			exit(0);
		case 0:
			handle_long_options(argv[0], long_option);
			break;
		case 'D':
			upf_cmdline_config.daemonize = 1;
			break;
		case 'c':
			upf_cmdline_config.config_file = optarg;
			break;
		case 'V':
			print_version(1);
			exit(0);
			break;
		default:
			/* catch unknown options *as well as* missing arguments. */
			fprintf(stderr, "%s: Error in command line options. Exiting.\n", argv[0]);
			exit(-1);
		}
	}

	if (argc > optind) {
		fprintf(stderr, "%s: Unsupported positional arguments on command line\n", argv[0]);
		exit(2);
	}
}

static void signal_handler(int signum)
{
	fprintf(stdout, "signal %u received\n", signum);

	switch (signum) {
	case SIGINT:
	case SIGTERM:
		LOGP(DLGLOBAL, LOGL_NOTICE, "Terminating due to signal %d\n", signum);
		quit++;
		break;
	case SIGABRT:
		osmo_generate_backtrace();
		/* in case of abort, we want to obtain a talloc report and
		 * then run default SIGABRT handler, who will generate coredump
		 * and abort the process. abort() should do this for us after we
		 * return, but program wouldn't exit if an external SIGABRT is
		 * received.
		 */
		talloc_report(tall_vty_ctx, stderr);
		talloc_report_full(tall_upf_ctx, stderr);
		signal(SIGABRT, SIG_DFL);
		raise(SIGABRT);
		break;
	case SIGUSR1:
		talloc_report(tall_vty_ctx, stderr);
		talloc_report_full(tall_upf_ctx, stderr);
		break;
	case SIGUSR2:
		talloc_report_full(tall_vty_ctx, stderr);
		break;
	default:
		break;
	}
}

static struct vty_app_info upf_vty_app_info = {
	.name = "OsmoUPF",
	.version = PACKAGE_VERSION,
	.copyright =
	"OsmoUPF - Osmocom User Plane Function implementation\r\n"
	"Copyright (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>\r\n"
	"License AGPLv3+: GNU AGPL version 3 or later <http://gnu.org/licenses/agpl-3.0.html>\r\n"
	"This is free software: you are free to change and redistribute it.\r\n"
	"There is NO WARRANTY, to the extent permitted by law.\r\n",
};

static const struct log_info_cat upf_default_categories[] = {
	[DREF] = {
		.name = "DREF",
		.description = "Reference Counting",
		.enabled = 0, .loglevel = LOGL_NOTICE,
		.color = OSMO_LOGCOLOR_DARKGREY,
	},
	[DPEER] = {
		.name = "DPEER",
		.description = "PFCP peer association",
		.enabled = 0, .loglevel = LOGL_NOTICE,
		.color = OSMO_LOGCOLOR_YELLOW,
	},
	[DSESSION] = {
		.name = "DSESSION",
		.description = "PFCP sessions",
		.enabled = 0, .loglevel = LOGL_NOTICE,
		.color = OSMO_LOGCOLOR_BLUE,
	},
	[DGTP] = {
		.name = "DGTP",
		.description = "GTP tunneling",
		.enabled = 0, .loglevel = LOGL_NOTICE,
		.color = OSMO_LOGCOLOR_PURPLE,
	},
	[DNFT] = {
		.name = "DNFT",
		.description = "GTP forwarding rules via linux netfilter",
		.enabled = 0, .loglevel = LOGL_NOTICE,
		.color = OSMO_LOGCOLOR_PURPLE,
	},
};

const struct log_info log_info = {
	.cat = upf_default_categories,
	.num_cat = ARRAY_SIZE(upf_default_categories),
};

int main(int argc, char **argv)
{
	int rc;
	void *tall_infra_ctx;

	/* Track the use of talloc NULL memory contexts */
	talloc_enable_null_tracking();

	osmo_fsm_set_dealloc_ctx(OTC_SELECT);

	tall_infra_ctx = talloc_named_const(NULL, 1, "osmo-upf");
	tall_upf_ctx = talloc_named_const(tall_infra_ctx, 1, "osmo-upf-main");
	upf_vty_app_info.tall_ctx = tall_upf_ctx;

	msgb_talloc_ctx_init(tall_upf_ctx, 0);
	osmo_signal_talloc_ctx_init(tall_upf_ctx);

	osmo_init_logging2(tall_infra_ctx, &log_info);
	log_set_print_category_hex(osmo_stderr_target, 0);
	log_set_print_category(osmo_stderr_target, 1);
	log_set_print_level(osmo_stderr_target, 1);
	log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME);
	log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END);
	log_set_print_extended_timestamp(osmo_stderr_target, 1);

	osmo_fsm_log_timeouts(true);
	osmo_fsm_log_addr(true);

	osmo_stats_init(tall_upf_ctx);

	g_upf_alloc(tall_upf_ctx);

	/* For --version, vty_init() must be called before handling options */
	vty_init(&upf_vty_app_info);

	ctrl_vty_init(tall_upf_ctx);
	logging_vty_add_cmds();
	osmo_talloc_vty_add_cmds();
	osmo_cpu_sched_vty_init(tall_upf_ctx);

	upf_vty_init();
	osmo_tdef_vty_groups_init(CONFIG_NODE, g_upf_tdef_groups);

	/* Parse options */
	handle_options(argc, argv);

	rc = vty_read_config_file(upf_cmdline_config.config_file, NULL);
	if (rc < 0) {
		LOGP(DLGLOBAL, LOGL_FATAL, "Failed to parse the config file: '%s'\n",
		     upf_cmdline_config.config_file);
		return 1;
	}

	/* start telnet VTY */
	rc = telnet_init_default(tall_upf_ctx, &g_upf, OSMO_VTY_PORT_UPF);
	if (rc < 0)
		return 2;

	/* start control interface, after reading config for ctrl_vty_get_bind_addr() */
	g_upf->ctrl = ctrl_interface_setup(g_upf, OSMO_CTRL_PORT_UPF, NULL);
	if (!g_upf->ctrl) {
		fprintf(stderr, "Failed to initialize control interface. Exiting.\n");
		return -1;
	}

	signal(SIGINT, &signal_handler);
	signal(SIGTERM, &signal_handler);
	signal(SIGABRT, &signal_handler);
	signal(SIGUSR1, &signal_handler);
	signal(SIGUSR2, &signal_handler);
	osmo_init_ignore_signals();

	if (upf_cmdline_config.daemonize) {
		rc = osmo_daemonize();
		if (rc < 0) {
			perror("Error during daemonize");
			return 6;
		}
	}

	if (upf_gtp_devs_open())
		return -1;

	if (upf_nft_init())
		return -1;

	if (upf_pfcp_listen())
		return -1;

	do {
		log_reset_context();
		osmo_select_main_ctx(0);

		/* If the user hits Ctrl-C the third time, just terminate immediately. */
		if (quit >= 3)
			break;

		/* Has SIGTERM been received (and not yet been handled)? */
		if (quit && !osmo_select_shutdown_requested()) {
			osmo_signal_dispatch(SS_L_GLOBAL, S_L_GLOBAL_SHUTDOWN, NULL);

			/* Request write-only mode in osmo_select_main_ctx() */
			osmo_select_shutdown_request();
			/* continue the main select loop until all write queues are serviced. */
		}
	} while (!osmo_select_shutdown_done());

	up_endpoint_free(&g_upf->pfcp.ep);
	upf_gtp_devs_close();

	upf_gtp_genl_close();

	upf_nft_free();

	/* Report the heap state of talloc contexts, then free, so both ASAN and Valgrind are happy... */
	talloc_report_full(tall_upf_ctx, stderr);
	talloc_free(tall_upf_ctx);

	log_fini();

	talloc_report_full(tall_infra_ctx, stderr);
	talloc_free(tall_infra_ctx);

	talloc_report(tall_vty_ctx, stderr);
	talloc_free(tall_vty_ctx);

	talloc_report_full(NULL, stderr);
	talloc_disable_null_tracking();
	return 0;
}