/*
 * (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/fsm.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>

enum pfcp_entity_peer_fsm_state {
	PFCP_ENTITY_PEER_ST_NOT_ASSOCIATED,
	PFCP_ENTITY_PEER_ST_ASSOCIATED,
	PFCP_ENTITY_PEER_ST_GRACEFUL_RELEASE,
	PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT,
};

static const struct value_string pfcp_entity_peer_fsm_event_names[] = {
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ),
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_RX_ASSOC_UPD_REQ),
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_RX_ASSOC_REL_REQ),
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_RX_SESSION_EST_REQ),
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_HEARTBEAT_FAILURE),
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO),
	OSMO_VALUE_STRING(PFCP_ENTITY_PEER_EV_SESSION_TERM),
	{}
};

static struct osmo_fsm pfcp_entity_peer_fsm;

static const struct osmo_tdef_state_timeout pfcp_entity_peer_fsm_timeouts[32] = {
	[PFCP_ENTITY_PEER_ST_GRACEFUL_RELEASE] = { .T = -21 },
};

/* Transition to a state, using the T timer defined in pfcp_entity_peer_fsm_timeouts.
 * Assumes local variable fi exists. */
#define pfcp_entity_peer_fsm_state_chg(state) \
	osmo_tdef_fsm_inst_state_chg(fi, state, \
				     pfcp_entity_peer_fsm_timeouts, \
				     osmo_pfcp_tdefs, \
				     5)

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

	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;

	LOGPFSMSLSRC(peer->fi, DREF, level, file, line,
		     "%s %s: now used by %s\n",
		     (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)
		osmo_fsm_inst_dispatch(peer->fi, PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO, NULL);
	return 0;
}

char *pfcp_entity_peer_remote_addr_str(struct pfcp_entity_peer *peer)
{
	struct osmo_sockaddr remote_addr = peer->remote_addr;

	/* 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(&remote_addr.u.sa, 0);

	return osmo_sockaddr_to_str_c(OTC_SELECT, &remote_addr);
}

static void pfcp_entity_peer_update_id(struct pfcp_entity_peer *peer)
{
	osmo_fsm_inst_update_id_f_sanitize(peer->fi, '-', "%s", pfcp_entity_peer_remote_addr_str(peer));
	LOGPFSML(peer->fi, LOGL_DEBUG, "Updated id\n");
}

struct pfcp_entity_peer *pfcp_entity_peer_alloc(struct pfcp_node_peer *node_peer, const struct osmo_sockaddr *remote_addr)
{
	struct pfcp_entity_peer *entity_peer;

	struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&pfcp_entity_peer_fsm, node_peer, NULL, LOGL_DEBUG, NULL);
	OSMO_ASSERT(fi);

	entity_peer = talloc(fi, struct pfcp_entity_peer);
	OSMO_ASSERT(entity_peer);
	fi->priv = entity_peer;

	*entity_peer = (struct pfcp_entity_peer) {
		.fi = fi,
		.node_peer = node_peer,
		.remote_addr = *remote_addr,
		.heartbeat_fi = NULL /* FIXME */,
		.use_count = {
			.talloc_object = entity_peer,
			.use_cb = pfcp_entity_peer_use_cb,
		},
	};
	osmo_use_count_make_static_entries(&entity_peer->use_count, entity_peer->use_count_buf, ARRAY_SIZE(entity_peer->use_count_buf));
	hash_init(entity_peer->sessions_by_up_seid);
	hash_init(entity_peer->sessions_by_cp_seid);

	pfcp_entity_peer_update_id(entity_peer);

	pfcp_node_peer_get(node_peer, fi->id);
	llist_add(&entity_peer->entry, &node_peer->entity_list);
	return entity_peer;
}

struct up_session *pfcp_entity_peer_find_up_session_by_up_seid(struct pfcp_entity_peer *entity_peer, uint64_t up_seid)
{
	struct up_session *session;
	hash_for_each_possible(entity_peer->sessions_by_up_seid, session, node_by_up_seid, up_seid) {
		if (up_seid == session->up_seid)
			return session;
	}
	return NULL;
}

struct up_session *pfcp_entity_peer_find_up_session_by_cp_f_seid(struct pfcp_entity_peer *entity_peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid)
{
	struct up_session *session;
	hash_for_each_possible(entity_peer->sessions_by_cp_seid, session, node_by_cp_seid, cp_f_seid->seid) {
		if (osmo_pfcp_ie_f_seid_cmp(&session->cp_f_seid, cp_f_seid) == 0)
			return session;
	}
	return NULL;
}

static int pfcp_entity_peer_fsm_timer_cb(struct osmo_fsm_inst *fi)
{
	//struct pfcp_entity_peer *peer = fi->priv;
	/* Return 1 to terminate FSM instance, 0 to keep running */
	return 1;
}

void pfcp_entity_peer_set_msg_ctx(struct pfcp_entity_peer *peer, struct osmo_pfcp_msg *m)
{
	OSMO_ASSERT(!m->ctx.peer_fi);

	m->ctx.peer_fi = peer->fi;
	m->ctx.peer_use_count = &peer->use_count;
	m->ctx.peer_use_token = (m->rx ? UP_USE_MSG_RX : UP_USE_MSG_TX);
	OSMO_ASSERT(osmo_use_count_get_put(m->ctx.peer_use_count, m->ctx.peer_use_token, 1) == 0);
}

void pfcp_entity_peer_remove_msg_ctx(struct pfcp_entity_peer *peer, struct osmo_pfcp_msg *m)
{
	OSMO_ASSERT(m->ctx.peer_fi && m->ctx.peer_fi == peer->fi);
	OSMO_ASSERT(osmo_use_count_get_put(m->ctx.peer_use_count, m->ctx.peer_use_token, -1) == 0);
	m->ctx.peer_fi = NULL;
	m->ctx.peer_use_count = NULL;
	m->ctx.peer_use_token = NULL;
}

struct osmo_pfcp_msg *pfcp_entity_peer_init_tx_req(struct pfcp_entity_peer *peer,
						   const struct osmo_sockaddr *dst_addr,
						   enum osmo_pfcp_message_type message_type)
{
	struct osmo_pfcp_msg *tx;
	tx = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, dst_addr, message_type);
	pfcp_entity_peer_set_msg_ctx(peer, tx);
	return tx;
}

struct osmo_pfcp_msg *pfcp_entity_peer_init_tx_resp(struct pfcp_entity_peer *peer,
						    struct osmo_pfcp_msg *in_reply_to,
						    enum osmo_pfcp_message_type message_type)
{
	struct osmo_pfcp_msg *tx;
	tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, in_reply_to, message_type);
	pfcp_entity_peer_set_msg_ctx(peer, tx);
	return tx;
}

static int pfcp_entity_peer_tx_assoc_setup_resp(struct pfcp_entity_peer *entity_peer, struct osmo_pfcp_msg *m, enum osmo_pfcp_cause cause)
{
	struct osmo_pfcp_msg *resp;

	resp = pfcp_entity_peer_init_tx_resp(entity_peer, m, OSMO_PFCP_MSGT_ASSOC_SETUP_RESP);

	resp->ies.assoc_setup_resp = (struct osmo_pfcp_msg_assoc_setup_resp) {
		.cause = cause,
		.recovery_time_stamp = osmo_pfcp_endpoint_get_recovery_timestamp(g_upf->pfcp.ep->pfcp_ep),
		.up_function_features_present = true,
		.up_function_features = entity_peer->node_peer->local_up_features,
	};

	if (osmo_pfcp_endpoint_tx(entity_peer->node_peer->up_endpoint->pfcp_ep, resp)) {
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Error sending response to this message,"
				  " cannot associate with peer\n");
		return -EIO;
	}
	return 0;
}

static int pfcp_entity_peer_tx_assoc_rel_resp(struct pfcp_entity_peer *peer, struct osmo_pfcp_msg *m, enum osmo_pfcp_cause cause)
{
	struct osmo_pfcp_msg *resp;

	resp = pfcp_entity_peer_init_tx_resp(peer, m, OSMO_PFCP_MSGT_ASSOC_RELEASE_RESP);

	resp->ies.assoc_release_resp = (struct osmo_pfcp_msg_assoc_release_resp) {
		.cause = cause,
	};

	if (osmo_pfcp_endpoint_tx(peer->node_peer->up_endpoint->pfcp_ep, resp)) {
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Error sending response to this message\n");
		return -EIO;
	}
	return 0;
}

static void pfcp_entity_peer_clear_sessions(struct pfcp_entity_peer *peer)
{
	struct up_session *session;
	int bkt;
	struct hlist_node *tmp;
	int count = 0;
	hash_for_each_safe(peer->sessions_by_up_seid, bkt, tmp, session, node_by_up_seid) {
		count += up_session_discard(session);
	}
	if (count)
		LOGPFSML(peer->fi, LOGL_NOTICE, "terminated %d sessions\n", count);
}

static void pfcp_entity_peer_rx_assoc_setup_req(struct pfcp_entity_peer *peer, struct osmo_pfcp_msg *m)
{
	struct osmo_fsm_inst *fi = peer->fi;
	enum osmo_pfcp_cause cause = OSMO_PFCP_CAUSE_REQUEST_ACCEPTED;

	if (fi->state == PFCP_ENTITY_PEER_ST_ASSOCIATED) {
		/* Retransmissions of the ACK response happen in pfcp_endpoint.c. So if we get this, it is a genuine
		 * duplicate association setup request. We could reject it. But why. Just "replace" with the new
		 * association. Continue. */
		/* If the peer has restarted, it has forgotten about all sessions. */
		if (peer->remote_recovery_timestamp != m->ies.assoc_setup_req.recovery_time_stamp) {
			LOGPFSML(fi, LOGL_NOTICE, "another Association Setup Request, with different Recovery Timestamp."
				 " Clearing sessions, sending ACK.\n");
			pfcp_entity_peer_clear_sessions(peer);
		} else {
			LOGPFSML(fi, LOGL_NOTICE, "another Association Setup Request, with same Recovery Timestamp."
				 " Keeping sessions, sending ACK.\n");
		}
	} else if (pfcp_entity_peer_fsm_state_chg(PFCP_ENTITY_PEER_ST_ASSOCIATED)) {
		/* Not allowed to transition to ST_ASSOCIATED */
		cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED;
	} else {
		/* Successfully transitioned to ST_ASSOCIATED */
		peer->remote_recovery_timestamp = m->ies.assoc_setup_req.recovery_time_stamp;
		if (m->ies.assoc_setup_req.cp_function_features_present)
			peer->node_peer->peer_cp_features = m->ies.assoc_setup_req.cp_function_features;
	}

	if (pfcp_entity_peer_tx_assoc_setup_resp(peer, m, cause)
	    || cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
		pfcp_entity_peer_fsm_state_chg(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT);

	LOGPFSML(fi, LOGL_NOTICE, "Peer associated, Node-Id=%s. Local UP features: [%s]; Peer CP features: [%s]\n",
		 osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &peer->node_peer->node_id),
		 osmo_pfcp_bits_to_str_c(OTC_SELECT, peer->node_peer->local_up_features.bits, osmo_pfcp_up_feature_strs),
		 osmo_pfcp_bits_to_str_c(OTC_SELECT, peer->node_peer->peer_cp_features.bits, osmo_pfcp_cp_feature_strs));
}

static void pfcp_entity_peer_rx_assoc_rel_req(struct pfcp_entity_peer *peer, struct osmo_pfcp_msg *m)
{
	struct osmo_fsm_inst *fi = peer->fi;
	pfcp_entity_peer_tx_assoc_rel_resp(peer, m, OSMO_PFCP_CAUSE_REQUEST_ACCEPTED);
	pfcp_entity_peer_fsm_state_chg(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT);
}

static void pfcp_entity_peer_rx_session_est_req(struct pfcp_entity_peer *peer, struct osmo_pfcp_msg *m)
{
	enum osmo_pfcp_cause cause = OSMO_PFCP_CAUSE_REQUEST_ACCEPTED;
	struct osmo_pfcp_msg *resp;
	struct up_session *session;

	session = pfcp_entity_peer_find_up_session_by_cp_f_seid(peer, &m->ies.session_est_req.cp_f_seid);
	if (!session)
		session = up_session_alloc(peer, &m->ies.session_est_req.cp_f_seid);
	if (!session) {
		cause = OSMO_PFCP_CAUSE_NO_RESOURCES_AVAILABLE;
		goto nack_response;
	}

	up_session_set_msg_ctx(session, m);

	if (osmo_fsm_inst_dispatch(session->fi, UP_SESSION_EV_RX_SESSION_EST_REQ, m)) {
		cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED;
		goto nack_response;
	}
	return;

nack_response:
	resp = pfcp_entity_peer_init_tx_resp(peer, m, OSMO_PFCP_MSGT_SESSION_EST_RESP);
	resp->h.seid = m->ies.session_est_req.cp_f_seid.seid;
	resp->h.seid_present = true;
	resp->ies.session_est_resp = (struct osmo_pfcp_msg_session_est_resp){
		.cause = cause,
	};
	osmo_pfcp_endpoint_tx(peer->node_peer->up_endpoint->pfcp_ep, resp);
	if (session)
		up_session_discard(session);
}

static void pfcp_entity_peer_not_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct pfcp_entity_peer *peer = fi->priv;

	switch (event) {

	case PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ:
		pfcp_entity_peer_rx_assoc_setup_req(peer, data);
		break;

	case PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO:
		/* Not associated and no pending messages. discard peer. */
		pfcp_entity_peer_fsm_state_chg(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT);
		return;

	default:
		OSMO_ASSERT(false);
	}
}

static void pfcp_entity_peer_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct pfcp_entity_peer *peer = fi->priv;

	switch (event) {

	case PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ:
		pfcp_entity_peer_rx_assoc_setup_req(peer, data);
		break;

	case PFCP_ENTITY_PEER_EV_RX_ASSOC_UPD_REQ:
		// FIXME
		break;

	case PFCP_ENTITY_PEER_EV_RX_SESSION_EST_REQ:
		pfcp_entity_peer_rx_session_est_req(peer, data);
		break;

	case PFCP_ENTITY_PEER_EV_HEARTBEAT_FAILURE:
		// FIXME
		break;

	case PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO:
		/* Stay associated. */
		return;

	default:
		OSMO_ASSERT(false);
	}
}

static void pfcp_entity_peer_associated_onleave(struct osmo_fsm_inst *fi, uint32_t next_state)
{
	struct pfcp_entity_peer *peer = fi->priv;
	if (next_state != PFCP_ENTITY_PEER_ST_ASSOCIATED)
		LOGPFSML(fi, LOGL_NOTICE, "Peer %s released\n",
			 osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &peer->node_peer->node_id));
}

static void pfcp_entity_peer_graceful_release_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	//struct pfcp_entity_peer *peer = fi->priv;
	// FIXME
}

static void pfcp_entity_peer_graceful_release_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct pfcp_entity_peer *peer = fi->priv;

	switch (event) {

	case PFCP_ENTITY_PEER_EV_HEARTBEAT_FAILURE:
		pfcp_entity_peer_fsm_state_chg(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT);
		break;

	case PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO:
		/* When there are still sessions, stay around. */
		if (!hash_empty(peer->sessions_by_up_seid))
			return;
		pfcp_entity_peer_fsm_state_chg(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT);
		return;

	default:
		OSMO_ASSERT(false);
	}
}

static void pfcp_entity_peer_wait_use_count_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	struct pfcp_entity_peer *peer = fi->priv;
	pfcp_entity_peer_clear_sessions(peer);
	if (!osmo_use_count_total(&peer->use_count))
		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
}

static void pfcp_entity_peer_wait_use_count_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct pfcp_entity_peer *peer = fi->priv;
	switch (event) {

	case PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO:
		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
		return;

	case PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ:
		pfcp_entity_peer_rx_assoc_setup_req(peer, data);
		break;

	default:
		OSMO_ASSERT(false);
	}
}

#define S(x)    (1 << (x))

static const struct osmo_fsm_state pfcp_entity_peer_fsm_states[] = {
	[PFCP_ENTITY_PEER_ST_NOT_ASSOCIATED] = {
		.name = "NOT_ASSOCIATED",
		.in_event_mask = 0
			| S(PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ)
			| S(PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO)
			,
		.out_state_mask = 0
			| S(PFCP_ENTITY_PEER_ST_ASSOCIATED)
			| S(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT)
			,
		.action = pfcp_entity_peer_not_associated_action,
	},
	[PFCP_ENTITY_PEER_ST_ASSOCIATED] = {
		.name = "ASSOCIATED",
		.in_event_mask = 0
			| S(PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ)
			| S(PFCP_ENTITY_PEER_EV_RX_ASSOC_UPD_REQ)
			| S(PFCP_ENTITY_PEER_EV_RX_SESSION_EST_REQ)
			| S(PFCP_ENTITY_PEER_EV_HEARTBEAT_FAILURE)
			| S(PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO)
			,
		.out_state_mask = 0
			| S(PFCP_ENTITY_PEER_ST_ASSOCIATED)
			| S(PFCP_ENTITY_PEER_ST_GRACEFUL_RELEASE)
			| S(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT)
			,
		.action = pfcp_entity_peer_associated_action,
		.onleave = pfcp_entity_peer_associated_onleave,
	},
	[PFCP_ENTITY_PEER_ST_GRACEFUL_RELEASE] = {
		.name = "GRACEFUL_RELEASE",
		.in_event_mask = 0
			| S(PFCP_ENTITY_PEER_EV_HEARTBEAT_FAILURE)
			| S(PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO)
			,
		.out_state_mask = 0
			| S(PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT)
			,
		.onenter = pfcp_entity_peer_graceful_release_onenter,
		.action = pfcp_entity_peer_graceful_release_action,
	},
	[PFCP_ENTITY_PEER_ST_WAIT_USE_COUNT] = {
		.name = "WAIT_USE_COUNT",
		.in_event_mask = 0
			| S(PFCP_ENTITY_PEER_EV_USE_COUNT_ZERO)
			| S(PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ)
			,
		.out_state_mask = 0
			| S(PFCP_ENTITY_PEER_ST_ASSOCIATED)
			,
		.onenter = pfcp_entity_peer_wait_use_count_onenter,
		.action = pfcp_entity_peer_wait_use_count_action,
	},
};

void pfcp_entity_peer_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
{
	struct pfcp_entity_peer *entity_peer = fi->priv;
	LOGPFSML(fi, LOGL_NOTICE, "Peer removed\n");
	llist_del(&entity_peer->entry);
	osmo_pfcp_endpoint_invalidate_ctx(entity_peer->node_peer->up_endpoint->pfcp_ep, fi);
	pfcp_node_peer_put(entity_peer->node_peer, fi->id);
}

static void pfcp_entity_peer_allstate_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	switch (event) {
	case PFCP_ENTITY_PEER_EV_SESSION_TERM:
		/* ignore */
		return;
	case PFCP_ENTITY_PEER_EV_RX_ASSOC_REL_REQ:
		pfcp_entity_peer_rx_assoc_rel_req(fi->priv, data);
		return;
	default:
		OSMO_ASSERT(false);
	}
}

static struct osmo_fsm pfcp_entity_peer_fsm = {
	.name = "pfcp_entity_peer",
	.log_subsys = DPEER,
	.states = pfcp_entity_peer_fsm_states,
	.num_states = ARRAY_SIZE(pfcp_entity_peer_fsm_states),
	.event_names = pfcp_entity_peer_fsm_event_names,
	.timer_cb = pfcp_entity_peer_fsm_timer_cb,
	.cleanup = pfcp_entity_peer_fsm_cleanup,
	.allstate_event_mask = 0
		| S(PFCP_ENTITY_PEER_EV_RX_ASSOC_REL_REQ)
		| S(PFCP_ENTITY_PEER_EV_SESSION_TERM)
		,
	.allstate_action = pfcp_entity_peer_allstate_action,
};

static __attribute__((constructor)) void pfcp_entity_peer_fsm_register(void)
{
	OSMO_ASSERT(osmo_fsm_register(&pfcp_entity_peer_fsm) == 0);
}

void pfcp_entity_peer_free(struct pfcp_entity_peer *peer)
{
	if (!peer)
		return;
	osmo_fsm_inst_term(peer->fi, OSMO_FSM_TERM_REGULAR, NULL);
}
