/*
 * (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 <stdbool.h>
#include <errno.h>
#include <string.h>
#include <net/if.h>
#include <linux/gtp.h>

#include <libgtpnl/gtpnl.h>
#include <libgtpnl/gtp.h>

#include <osmocom/core/socket.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/sockaddr_str.h>

#include <osmocom/upf/upf.h>
#include <osmocom/upf/upf_gtp.h>
#include <osmocom/upf/upf_gtpu_echo.h>

#define LOG_GTP_TUN(TUN, LEVEL, FMT, ARGS...) \
	LOGP(DGTP, LEVEL, "%s: " FMT, upf_gtp_tunend_to_str_c(OTC_SELECT, (TUN)), ##ARGS)

int upf_gtp_dev_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_dev *dev)
{
	uint16_t v0_port;
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
	OSMO_STRBUF_PRINTF(sb, "%s", dev->name ? : "null");
	if (dev->name && dev->ifidx)
		OSMO_STRBUF_PRINTF(sb, " [%u]", dev->ifidx);
	if (dev->sgsn_mode)
		OSMO_STRBUF_PRINTF(sb, " (SGSN)");
	OSMO_STRBUF_PRINTF(sb, " ");
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &dev->gtpv1.local_addr);
	v0_port = osmo_sockaddr_port(&dev->gtpv0.local_addr.u.sa);
	if (dev->gtpv0.enabled && v0_port)
		OSMO_STRBUF_PRINTF(sb, "/%u", v0_port);
	return sb.chars_needed;
}

char *upf_gtp_dev_to_str_c(void *ctx, const struct upf_gtp_dev *dev)
{
	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_gtp_dev_to_str_buf, dev)
}

struct upf_gtp_dev *upf_gtp_dev_find_by_name(const char *name)
{
	struct upf_gtp_dev *dev;
	llist_for_each_entry(dev, &g_upf->tunend.devs, entry) {
		if (!strcmp(name, dev->name))
			return dev;
	}
	return NULL;
}

struct upf_gtp_dev *upf_gtp_dev_find_by_local_addr(const struct osmo_sockaddr *local_addr)
{
	struct upf_gtp_dev *dev;
	struct upf_gtp_dev *dev_any = NULL;
	struct osmo_sockaddr needle = *local_addr;

	llist_for_each_entry(dev, &g_upf->tunend.devs, entry) {
		/* To leave the port number out of the cmp, set the needle's port to match */
		osmo_sockaddr_set_port(&needle.u.sa, osmo_sockaddr_port(&dev->gtpv1.local_addr.u.sa));

		if (!osmo_sockaddr_cmp(&needle, &dev->gtpv1.local_addr))
			return dev;
		if (osmo_sockaddr_is_any(&dev->gtpv1.local_addr) == 1)
			dev_any = dev;
	}
	/* No 1:1 match found, but there is a dev listening on ANY? Return that.
	 * If there is no such dev, return NULL. */
	return dev_any;
}

struct upf_gtp_dev *upf_gtp_dev_first()
{
	return llist_first_entry_or_null(&g_upf->tunend.devs, struct upf_gtp_dev, entry);
}

/* Tell the kernel to remove the GTP device. Called implicitly by talloc_free() (see upf_gtp_dev_destruct()). */
static int upf_gtp_dev_delete(struct upf_gtp_dev *dev)
{
	int rc;
	if (!dev->name)
		return 0;
	rc = gtp_dev_destroy(dev->name);
	if (rc < 0) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Error while deleting device: %s\n", strerror(errno));
		return rc;
	}
	LOG_GTP_DEV(dev, LOGL_NOTICE, "Deleted GTP device\n");
	dev->name = NULL;
	return 0;
}

static int upf_gtp_dev_destruct(struct upf_gtp_dev *dev);

/* Allocate state for one GTP device, add to g_upf->tunend.devs and return the created device. If state for the device of
 * that name already exists, do nothing and return NULL. */
static struct upf_gtp_dev *upf_gtp_dev_alloc(const char *name, const char *local_addr)
{
	struct upf_gtp_dev *dev = upf_gtp_dev_find_by_name(name);
	struct osmo_sockaddr_str addr_conv;
	local_addr = local_addr ? : "0.0.0.0";
	if (dev) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Device already exists. Cannot create %s %s\n", name, local_addr);
		return NULL;
	}
	dev = talloc(g_upf, struct upf_gtp_dev);
	*dev = (struct upf_gtp_dev){
		.name = talloc_strdup(dev, name),
		.gtpv0.ofd.fd = -1,
		.gtpv1.ofd.fd = -1,
	};
	INIT_LLIST_HEAD(&dev->tunnels);
	hash_init(dev->tunnels_by_local_f_teid);

	osmo_sockaddr_str_from_str(&addr_conv, local_addr, PORT_GTP0_U);

	osmo_sockaddr_str_to_sockaddr(&addr_conv, &dev->gtpv0.local_addr.u.sas);

	addr_conv.port = PORT_GTP1_U;
	osmo_sockaddr_str_to_sockaddr(&addr_conv, &dev->gtpv1.local_addr.u.sas);

	/* Need to add to list before setting up the destructor. A talloc_free() does automagically remove from the
	 * list. */
	llist_add(&dev->entry, &g_upf->tunend.devs);

	talloc_set_destructor(dev, upf_gtp_dev_destruct);

	return dev;
}

static int dev_resolve_ifidx(struct upf_gtp_dev *dev)
{
	int rc;

	dev->ifidx = if_nametoindex(dev->name);
	if (dev->ifidx == 0) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "No such device: '%s'\n", dev->name);
		talloc_free(dev);
		return -EIO;
	}
	/* Let's try something to see if talking to the device works. */
	errno = 0;
	rc = gtp_list_tunnel(g_upf->tunend.genl_id, g_upf->tunend.nl);
	if (errno)
		rc = -errno;
	else if (rc)
		rc = -EINVAL;
	if (rc) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Failed to open GTP device: %s\n", strerror(-rc));
		talloc_free(dev);
		return rc;
	}

	LOG_GTP_DEV(dev, LOGL_NOTICE, "GTP device ready (ifidx=%u)\n", dev->ifidx);
	return 0;
}

static int upf_gtp_dev_create(struct upf_gtp_dev *dev, int gtp0_fd, int gtp1_fd)
{
	if (dev->sgsn_mode)
		return gtp_dev_create_sgsn(-1, dev->name, gtp0_fd, gtp1_fd);
	return gtp_dev_create(-1, dev->name, gtp0_fd, gtp1_fd);
}

int upf_gtp_dev_open(const char *name, bool create_gtp_dev, const char *local_addr, bool listen_for_gtpv0, bool sgsn_mode)
{
	const struct osmo_sockaddr any = {
		.u.sin = {
			.sin_family = AF_INET,
			.sin_port = 0,
			.sin_addr = {
				.s_addr = INADDR_ANY,
			},
		},
	};
	int rc;
	struct upf_gtp_dev *dev;

	if (g_upf->tunend.mockup) {
		LOGP(DGTP, LOGL_NOTICE, "tunend/mockup active: not opening GTP device '%s'\n", name);
		return 0;
	}

	dev = upf_gtp_dev_alloc(name, local_addr);
	if (!dev)
		return -EIO;

	dev->sgsn_mode = sgsn_mode;

	rc = upf_gtp_genl_ensure_open();
	if (rc) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot set up GTP device, failed to open mnl_socket\n");
		return rc;
	}

	if (listen_for_gtpv0) {
		rc = osmo_sock_init_osa_ofd(&dev->gtpv0.ofd, SOCK_DGRAM, 0, &dev->gtpv0.local_addr, &any,
					    OSMO_SOCK_F_BIND);
		if (rc < 0) {
			LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot bind GTPv0 on %s (rc=%d)\n",
				    osmo_sockaddr_to_str_c(OTC_SELECT, &dev->gtpv0.local_addr), rc);
			talloc_free(dev);
			return -EIO;
		}
		dev->gtpv0.enabled = true;
		LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv0 bound\n");
	}

	/* GTPv1 */
	rc = osmo_sock_init_osa_ofd(&dev->gtpv1.ofd, SOCK_DGRAM, 0, &dev->gtpv1.local_addr, &any,
				    OSMO_SOCK_F_BIND);
	if (rc < 0) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot bind GTPv1 (rc=%d)\n", rc);
		talloc_free(dev);
		return -EIO;
	}
	LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv1 bound\n");

	if (create_gtp_dev) {
		int gtp0_fd = listen_for_gtpv0 ? dev->gtpv0.ofd.fd : -1;
		int gtp1_fd = dev->gtpv1.ofd.fd;

		rc = upf_gtp_dev_create(dev, gtp0_fd, gtp1_fd);
		if (rc == -EEXIST && gtp_dev_destroy(dev->name) == 0) {
			LOG_GTP_DEV(dev, LOGL_ERROR, "deleted GTP device from unclean shutdown\n");
			rc = upf_gtp_dev_create(dev, gtp0_fd, gtp1_fd);
		}

		if (rc < 0) {
			LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot create GTP device: rc=%d\n", rc);
			/* name = NULL: signal to the destructor that it does not need to delete the device */
			dev->name = NULL;
			talloc_free(dev);
			return -EIO;
		}

		LOG_GTP_DEV(dev, LOGL_NOTICE, "created GTP device\n");
		dev->created = true;
	}

	rc = dev_resolve_ifidx(dev);
	if (rc) {
		talloc_free(dev);
		return -EIO;
	}

	upf_gtpu_echo_setup(dev);

	return 0;
}

void upf_gtp_devs_close()
{
	struct upf_gtp_dev *dev;
	while ((dev = llist_first_entry_or_null(&g_upf->tunend.devs, struct upf_gtp_dev, entry)))
		talloc_free(dev);
}

void upf_gtp_genl_close()
{
	if (!g_upf->tunend.nl)
		return;
	genl_socket_close(g_upf->tunend.nl);
	g_upf->tunend.nl = NULL;
	g_upf->tunend.genl_id = -1;

	LOGP(DGTP, LOGL_NOTICE, "Closed mnl_socket\n");
}

/* Open an MNL socket which allows to create and remove GTP devices (requires CAP_NET_ADMIN). */
int upf_gtp_genl_ensure_open()
{
	/* Already open? */
	if (g_upf->tunend.nl && g_upf->tunend.genl_id >= 0)
		return 0;

	/* sanity / paranoia: if re-opening, make sure the previous socket is closed */
	if (g_upf->tunend.nl)
		upf_gtp_genl_close();

	g_upf->tunend.nl = genl_socket_open();
	if (!g_upf->tunend.nl) {
		LOGP(DGTP, LOGL_ERROR, "Cannot open mnl_socket: %s\n", strerror(errno));
		return -EIO;
	}

	g_upf->tunend.genl_id = genl_lookup_family(g_upf->tunend.nl, "gtp");
	if (g_upf->tunend.genl_id < 0) {
		LOGP(DGTP, LOGL_ERROR, "genl family 'gtp' not found\n");
		return -ENOTSUP;
	}

	LOGP(DGTP, LOGL_NOTICE, "Opened mnl_socket\n");
	return 0;
}

struct upf_gtp_tunend {
	struct llist_head entry; /* item in (struct upf_gtp_dev)->tunnels */
	struct hlist_node node_by_local_f_teid; /* item in g_upf->gtp.pdrs_by_local_f_teid */

	struct upf_gtp_dev *dev;
	struct upf_tunend desc;
	bool active;
};

static int upf_gtp_tunend_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_tunend *tun)
{
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
	/* "tunend{dev=apn0 access(GTP-r=1.2.3.4 TEID:l=0x1234,r=0x5678) core(UE-l=10.9.8.7)}" */
	OSMO_STRBUF_PRINTF(sb, "tunend{dev=%s access(GTP-r=", tun->dev->name);
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &tun->desc.access.remote.addr);
	OSMO_STRBUF_PRINTF(sb, " TEID:l=0x%x,r=0x%x) core(UE-l=",
			   tun->desc.access.local.teid, tun->desc.access.remote.teid);
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &tun->desc.core.ue_local_addr);
	OSMO_STRBUF_PRINTF(sb, ")}");
	return sb.chars_needed;
}

static char *upf_gtp_tunend_to_str_c(void *ctx, const struct upf_gtp_tunend *tun)
{
	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_gtp_tunend_to_str_buf, tun)
}

static int upf_gtp_tunend_deactivate(struct upf_gtp_tunend *tun);

static int upf_gtp_tunend_destruct(struct upf_gtp_tunend *tun)
{
	if (tun->active)
		upf_gtp_tunend_deactivate(tun);
	hash_del(&tun->node_by_local_f_teid);
	llist_del(&tun->entry);
	return 0;
}

#define tunend_validate(TUNEND) \
	do { \
		OSMO_ASSERT(osmo_sockaddr_port(&(TUNEND)->access.local.addr.u.sa) == 0); \
		OSMO_ASSERT(osmo_sockaddr_port(&(TUNEND)->access.remote.addr.u.sa) == 0); \
		OSMO_ASSERT(osmo_sockaddr_port(&(TUNEND)->core.ue_local_addr.u.sa) == 0); \
	} while (0)

static struct upf_gtp_tunend *upf_gtp_tunend_alloc(struct upf_gtp_dev *dev, const struct upf_tunend *desc)
{
	struct upf_gtp_tunend *tun = talloc(dev, struct upf_gtp_tunend);
	OSMO_ASSERT(tun);
	tunend_validate(desc);
	*tun = (struct upf_gtp_tunend){
		.dev = dev,
		.desc = *desc,
	};
	hash_add(dev->tunnels_by_local_f_teid, &tun->node_by_local_f_teid, tun->desc.access.local.teid);
	llist_add(&tun->entry, &dev->tunnels);
	talloc_set_destructor(tun, upf_gtp_tunend_destruct);
	return tun;
}

static struct gtp_tunnel *upf_gtp_tunend_to_gtp_tunnel(struct upf_gtp_tunend *tun)
{
	struct gtp_tunnel *t;

	if (tun->desc.core.ue_local_addr.u.sas.ss_family != AF_INET
	    || tun->desc.access.remote.addr.u.sas.ss_family != AF_INET) {
		LOG_GTP_TUN(tun, LOGL_ERROR, "Only capabale of IPv4\n");
		return NULL;
	}

	t = gtp_tunnel_alloc();
	OSMO_ASSERT(t);
	gtp_tunnel_set_ifidx(t, tun->dev->ifidx);
	gtp_tunnel_set_version(t, GTP_V1);
	gtp_tunnel_set_i_tei(t, tun->desc.access.local.teid);
	gtp_tunnel_set_o_tei(t, tun->desc.access.remote.teid);
	gtp_tunnel_set_sgsn_ip4(t, &tun->desc.access.remote.addr.u.sin.sin_addr);
	gtp_tunnel_set_ms_ip4(t, &tun->desc.core.ue_local_addr.u.sin.sin_addr);
	return t;
}

int upf_gtp_tunend_activate(struct upf_gtp_tunend *tun)
{
	int rc;
	struct gtp_tunnel *t;

	if (tun->active)
		return -EALREADY;

	t = upf_gtp_tunend_to_gtp_tunnel(tun);
	if (!t)
		return -ENOTSUP;

	errno = 0;
	rc = gtp_add_tunnel(g_upf->tunend.genl_id, g_upf->tunend.nl, t);
	if (errno) {
		rc = -errno;
	} else if (rc) {
		rc = -EINVAL;
	} else {
		tun->active = true;
	}

	gtp_tunnel_free(t);
	return rc;
}

static struct upf_gtp_tunend *upf_gtp_dev_tunend_find(struct upf_gtp_dev *dev, const struct upf_tunend *tunend)
{
	struct upf_gtp_tunend *tun;
	tunend_validate(tunend);

	hash_for_each_possible(dev->tunnels_by_local_f_teid, tun, node_by_local_f_teid, tunend->access.local.teid) {
		if (upf_gtp_tunend_cmp(tunend, &tun->desc))
			continue;
		return tun;
	}
	return NULL;
}

int upf_gtp_dev_tunend_add(struct upf_gtp_dev *dev, const struct upf_tunend *tunend)
{
	struct upf_gtp_tunend *tun;
	tunend_validate(tunend);
	tun = upf_gtp_dev_tunend_find(dev, tunend);
	if (!tun)
		tun = upf_gtp_tunend_alloc(dev, tunend);
	if (tun->active)
		return 0;
	return upf_gtp_tunend_activate(tun);
}

int upf_gtp_dev_tunend_del(struct upf_gtp_dev *dev, const struct upf_tunend *tunend)
{
	struct upf_gtp_tunend *tun;
	int rc;
	tunend_validate(tunend);
	tun = upf_gtp_dev_tunend_find(dev, tunend);
	if (!tun)
		return 0;
	if (tun->active) {
		rc = upf_gtp_tunend_deactivate(tun);
		if (rc)
			return rc;
	}
	talloc_free(tun);
	return 0;
}

static int upf_gtp_tunend_deactivate(struct upf_gtp_tunend *tun)
{
	int rc;
	struct gtp_tunnel *t;

	if (!tun->active) {
		LOG_GTP_TUN(tun, LOGL_ERROR, "Cannot deactivate, not active\n");
		return -EINVAL;
	}

	t = upf_gtp_tunend_to_gtp_tunnel(tun);
	if (!t)
		return -EINVAL;

	rc = gtp_del_tunnel(g_upf->tunend.genl_id, g_upf->tunend.nl, t);
	if (rc)
		LOG_GTP_TUN(tun, LOGL_ERROR, "Failed to delete tunnel\n");
	else
		tun->active = false;

	gtp_tunnel_free(t);
	return rc;
}

static int upf_gtp_dev_destruct(struct upf_gtp_dev *dev)
{
	struct upf_gtp_tunend *t;
	/* Destruct and clean up all active tunnels before deleting the device */
	while ((t = llist_first_entry_or_null(&dev->tunnels, struct upf_gtp_tunend, entry)))
		talloc_free(t);
	llist_del(&dev->entry);
	/* osmo_fd_close() is a noop if ofd.fd == -1 */
	osmo_fd_close(&dev->gtpv0.ofd);
	osmo_fd_close(&dev->gtpv1.ofd);
	if (dev->created)
		upf_gtp_dev_delete(dev);
	return 0;
}

int upf_gtp_tunend_cmp(const struct upf_tunend *a, const struct upf_tunend *b)
{
	int r;

	if (a == b)
		return 0;
	if (!a)
		return -1;
	if (!b)
		return 1;

#define CMP_MEMB(MEMB) OSMO_CMP(a->MEMB, b->MEMB)
	if ((r = CMP_MEMB(access.local.teid)))
		return r;
	if ((r = CMP_MEMB(access.remote.teid)))
		return r;
	return osmo_sockaddr_cmp(&a->access.remote.addr, &b->access.remote.addr);
}
