/*
 * (C) 2021-2025 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 <errno.h>

#include <osmocom/core/logging.h>
#include <osmocom/core/tdef.h>
#include <osmocom/core/utils.h>

#include <osmocom/pfcp/pfcp_msg.h>
#include <osmocom/pfcp/pfcp_endpoint.h>

#include <osmocom/upf/upf.h>
#include <osmocom/upf/pfcp_entity_peer.h>
#include <osmocom/upf/pfcp_node_peer.h>
#include <osmocom/upf/up_endpoint.h>
#include <osmocom/upf/up_session.h>


static int pfcp_node_peer_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line)
{
	struct pfcp_node_peer *peer = e->use_count->talloc_object;
	int32_t total;
	int level;

	if (!peer) /* already in pfcp_node_peer_free() */
		return 0;

	if (!e->use)
		return -EINVAL;

	total = osmo_use_count_total(&peer->use_count);

	if (total == 0
	    || (total == 1 && old_use_count == 0 && e->count == 1))
		level = LOGL_INFO;
	else
		level = LOGL_DEBUG;

	LOGPSRC(DREF, level, file, line,
		"pfcp_node_peer(%s): %s %s: now used by %s\n",
		pfcp_node_peer_node_id_str(peer),
		(e->count - old_use_count) > 0 ? "+" : "-", e->use,
		osmo_use_count_to_str_c(OTC_SELECT, &peer->use_count));

	if (e->count < 0)
		return -ERANGE;

	if (total == 0)
		pfcp_node_peer_free(peer);
	return 0;
}

char *pfcp_node_peer_node_id_str(struct pfcp_node_peer *node_peer)
{
	struct osmo_pfcp_ie_node_id node_id = node_peer->node_id;

	switch (node_id.type) {
	case OSMO_PFCP_NODE_ID_T_IPV4:
	case OSMO_PFCP_NODE_ID_T_IPV6:
		/* Zero the port, it is not interesting information. The port for PFCP is defined fixed, and there is no use
		 * printing it in the logs */
		osmo_sockaddr_set_port(&node_id.ip.u.sa, 0);
		break;
	default:
		break;
	}

	return osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &node_id);
}


struct pfcp_node_peer *pfcp_node_peer_alloc(struct up_endpoint *up_endpoint, const struct osmo_pfcp_ie_node_id *node_id)
{
	struct pfcp_node_peer *node_peer;

	node_peer = talloc(up_endpoint, struct pfcp_node_peer);
	OSMO_ASSERT(node_peer);

	*node_peer = (struct pfcp_node_peer) {
		.up_endpoint = up_endpoint,
		.node_id = *node_id,
		.use_count = {
			.talloc_object = node_peer,
			.use_cb = pfcp_node_peer_use_cb,
		},
	};
	osmo_use_count_make_static_entries(&node_peer->use_count, node_peer->use_count_buf, ARRAY_SIZE(node_peer->use_count_buf));
	INIT_LLIST_HEAD(&node_peer->entity_list);

	osmo_pfcp_bits_set(node_peer->local_up_features.bits, OSMO_PFCP_UP_FEAT_BUNDL, true);
	osmo_pfcp_bits_set(node_peer->local_up_features.bits, OSMO_PFCP_UP_FEAT_RTTL, true);
	osmo_pfcp_bits_set(node_peer->local_up_features.bits, OSMO_PFCP_UP_FEAT_FTUP, true);

	llist_add(&node_peer->entry, &up_endpoint->pfcp_node_peer_list);
	return node_peer;
}

struct pfcp_entity_peer *pfcp_node_peer_find_entity_by_remote_addr(struct pfcp_node_peer *node_peer, const struct osmo_sockaddr *remote_addr)
{
	struct pfcp_entity_peer *entity_peer;
	llist_for_each_entry(entity_peer, &node_peer->entity_list, entry) {
		if (osmo_sockaddr_cmp(&entity_peer->remote_addr, remote_addr))
			continue;
		return entity_peer;
	}
	return NULL;
}

void pfcp_node_peer_free(struct pfcp_node_peer *node_peer)
{
	if (!node_peer)
		return;

	LOG_PNP(node_peer, LOGL_NOTICE, "removed\n");

	struct pfcp_entity_peer *entity_peer;
	/* Avoid recursive free over user_count reaching zero while removing last pfcp_entity_peer: */
	node_peer->use_count.talloc_object = NULL;
	while ((entity_peer = llist_first_entry_or_null(&node_peer->entity_list, struct pfcp_entity_peer, entry)))
		pfcp_entity_peer_free(entity_peer);

	llist_del(&node_peer->entry);
}
