/*
 * (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/pfcp/pfcp_endpoint.h>
#include <osmocom/pfcp/pfcp_msg.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 struct pfcp_entity_peer *up_endp_find_msg_entity_peer(const struct up_endpoint *up_ep, struct osmo_pfcp_msg *m)
{
	struct pfcp_node_peer *node_peer;

	/* Look up over remote PFCP Entity identified by remote IP address and our local PFCP Entity */
	llist_for_each_entry(node_peer, &up_ep->pfcp_node_peer_list, entry) {
		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, &m->remote_addr))
				continue;
			return entity_peer;
		}
	}
	return NULL;
}

static void up_endpoint_set_msg_ctx(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m, struct osmo_pfcp_msg *req)
{
	struct up_endpoint *up_ep = osmo_pfcp_endpoint_get_cfg(ep)->priv;
	struct pfcp_entity_peer *entity_peer = NULL;

	/* If this is a response to an earlier request, just take the msg context from the request message. */
	if (req) {
		if (!m->ctx.peer_fi && req->ctx.peer_fi)
			pfcp_entity_peer_set_msg_ctx(req->ctx.peer_fi->priv, m);
		if (!m->ctx.session_fi && req->ctx.session_fi)
			pfcp_entity_peer_set_msg_ctx(req->ctx.session_fi->priv, m);
	}

	/* From the Node Id, find the matching PFCP Node */
	if (m->ctx.peer_fi) {
		entity_peer = m->ctx.peer_fi->priv;
	} else {
		entity_peer = up_endp_find_msg_entity_peer(up_ep, m);
		if (entity_peer)
			pfcp_entity_peer_set_msg_ctx(entity_peer, m);
	}


	/* Find a session, if the header is parsed yet and contains a SEID */
	if (entity_peer && !m->ctx.session_fi && m->h.seid_present) {
		struct up_session *session;
		session = pfcp_entity_peer_find_up_session_by_up_seid(entity_peer, m->h.seid);
		if (session) {
			up_session_set_msg_ctx(session, m);
		}
	}
}

static void up_ep_rx_not_impl_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m,
				  enum osmo_pfcp_message_type resp_msgt, enum osmo_pfcp_cause cause)
{
	struct osmo_pfcp_msg *tx;
	enum osmo_pfcp_cause *tx_cause;
	OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "message type not implemented\n");
	tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, resp_msgt);
	tx_cause = osmo_pfcp_msg_cause(tx);
	if (tx_cause)
		*tx_cause = cause;
	osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx);
}

static void up_ep_rx_pfd_mgmt_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_PFD_MGMT_RESP, OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED);
}

static void up_ep_rx_assoc_setup_req(struct up_endpoint *up_ep, struct osmo_pfcp_msg *m)
{
	struct pfcp_entity_peer *entity_peer = m->ctx.peer_fi ? m->ctx.peer_fi->priv : NULL;
	struct osmo_pfcp_ie_node_id *node_id = osmo_pfcp_msg_node_id(m);
	struct pfcp_node_peer *node_peer;
	OSMO_ASSERT(node_id);

	/* If PFCP entity belonged to a different PFCP Node (ie. Node Id changed), drop
	 * old entity and re-create a new one on the new Node: */
	if (entity_peer && osmo_pfcp_ie_node_id_cmp(&entity_peer->node_peer->node_id, node_id)) {
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "PFCP Entity Node-Id changed: %s -> %s!\n",
				  osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &entity_peer->node_peer->node_id),
				  osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, node_id));
		pfcp_entity_peer_remove_msg_ctx(entity_peer, m);
		pfcp_entity_peer_free(entity_peer);
		entity_peer = NULL;
	}

	if (!entity_peer) {
		OSMO_ASSERT(node_id);
		node_peer = up_endpoint_find_pfcp_node_peer(up_ep, node_id);
		if (!node_peer) {
			node_peer = pfcp_node_peer_alloc(up_ep, node_id);
			OSMO_ASSERT(node_peer);
		}
		entity_peer = pfcp_node_peer_find_entity_by_remote_addr(node_peer, &m->remote_addr);
		if (!entity_peer) {
			entity_peer = pfcp_entity_peer_alloc(node_peer, &m->remote_addr);
			OSMO_ASSERT(entity_peer);
		}
		pfcp_entity_peer_set_msg_ctx(entity_peer, m);
	}
	osmo_fsm_inst_dispatch(entity_peer->fi, PFCP_ENTITY_PEER_EV_RX_ASSOC_SETUP_REQ, (void *)m);
}

static void up_ep_rx_assoc_upd_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	if (!m->ctx.peer_fi) {
		struct osmo_pfcp_msg *tx;
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot update association\n");
		tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_ASSOC_UPDATE_RESP);
		osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx);
		return;
	}
	osmo_fsm_inst_dispatch(m->ctx.peer_fi, PFCP_ENTITY_PEER_EV_RX_ASSOC_UPD_REQ, (void *)m);
}

static void up_ep_rx_assoc_rel_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	if (!m->ctx.peer_fi) {
		struct osmo_pfcp_msg *tx;
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated. Sending ACK response anyway\n");
		tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_ASSOC_RELEASE_RESP);
		osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx);
		return;
	}
	osmo_fsm_inst_dispatch(m->ctx.peer_fi, PFCP_ENTITY_PEER_EV_RX_ASSOC_REL_REQ, (void *)m);
}

static void up_ep_rx_node_report_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_NODE_REPORT_RESP, OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED);
}

static void up_ep_rx_session_set_del_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_SESSION_SET_DEL_RESP,
			      OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED);
}

/* Validate received F-SEID IP address */
static enum osmo_pfcp_cause up_ep_validate_cp_f_seid_addr(const struct up_endpoint *up_ep,
							  const struct osmo_pfcp_ie_f_seid *f_seid)
{
	const struct osmo_sockaddr *local_addr;

	OSMO_ASSERT(up_ep);
	OSMO_ASSERT(f_seid);

	/* Validate the F-SEID contains an IP address we can send requests to later on: */
	local_addr = osmo_pfcp_endpoint_get_local_addr(up_ep->pfcp_ep);
	OSMO_ASSERT(local_addr);
	switch (local_addr->u.sa.sa_family) {
	case AF_INET:
		if (!f_seid->ip_addr.v4_present)
			return OSMO_PFCP_CAUSE_MANDATORY_IE_INCORRECT;
		break;
	case AF_INET6:
		if (!f_seid->ip_addr.v4_present &&
		    !f_seid->ip_addr.v6_present)
			return OSMO_PFCP_CAUSE_MANDATORY_IE_INCORRECT;
		break;
	default:
		OSMO_ASSERT(0);
	}
	return OSMO_PFCP_CAUSE_REQUEST_ACCEPTED;
}

static void up_ep_rx_session_est_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	struct osmo_pfcp_msg *resp;
	enum osmo_pfcp_cause cause;

	cause = up_ep_validate_cp_f_seid_addr(up_ep, &m->ies.session_est_req.cp_f_seid);
	if (cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) {
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Remote CP F-SEID IP address invalid\n");
		goto nack_response;
	}

	if (!m->ctx.peer_fi) {
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot establish session\n");
		cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC;
		goto nack_response;
	}

	osmo_fsm_inst_dispatch(m->ctx.peer_fi, PFCP_ENTITY_PEER_EV_RX_SESSION_EST_REQ, (void *)m);
	return;

nack_response:
	resp = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_SESSION_EST_RESP);
	resp->ies.session_est_resp.cause = cause;
	osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, resp);
}

static void up_ep_rx_session_mod_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	struct osmo_pfcp_msg *resp;
	enum osmo_pfcp_cause cause;

	if (m->ies.session_mod_req.cp_f_seid_present) {
		cause = up_ep_validate_cp_f_seid_addr(up_ep, &m->ies.session_mod_req.cp_f_seid);
		if (cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) {
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Remote CP F-SEID IP address invalid\n");
			goto nack_response;
		}
	}

	if (!m->ctx.session_fi) {
		/* Session not found. */
		if (!m->ctx.peer_fi) {
			/* Not even the peer is associated. */
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot modify session\n");
			cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC;
			goto nack_response;
		} else {
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR,
					  "No established session with SEID=0x%"PRIx64", cannot modify\n",
					  m->h.seid);
			cause = OSMO_PFCP_CAUSE_SESSION_CTX_NOT_FOUND;
			goto nack_response;
		}
	}

	osmo_fsm_inst_dispatch(m->ctx.session_fi, UP_SESSION_EV_RX_SESSION_MOD_REQ, (void *)m);
	return;

nack_response:
	resp = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_SESSION_MOD_RESP);
	resp->ies.session_mod_resp.cause = cause;
	osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, resp);
}

static void up_ep_rx_session_del_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	struct osmo_pfcp_msg *resp;
	enum osmo_pfcp_cause cause;

#if 0
	/* Can be enabled once libosmo-pfcp struct osmo_pfcp_msg_session_del_req supports the IE: */
	if (m->ies.session_del_req.cp_f_seid_present) {
		cause = up_ep_validate_cp_f_seid_addr(up_ep, &m->ies.session_mod_req.cp_f_seid);
		if (cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) {
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Remote CP F-SEID IP address invalid\n");
			goto nack_response;
		}
	}
#endif

	if (!m->ctx.session_fi) {
		/* Session not found. */
		if (!m->ctx.peer_fi) {
			/* Not even the peer is associated. */
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot delete session\n");
			cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC;
			goto nack_response;
		} else {
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR,
					  "No established session with SEID=0x%"PRIx64", cannot delete\n",
					  m->h.seid);
			cause = OSMO_PFCP_CAUSE_SESSION_CTX_NOT_FOUND;
			goto nack_response;
		}
	}

	osmo_fsm_inst_dispatch(m->ctx.session_fi, UP_SESSION_EV_RX_SESSION_DEL_REQ, (void *)m);
	return;

nack_response:
	resp = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_SESSION_DEL_RESP);
	resp->ies.session_del_resp.cause = cause;
	osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, resp);
}

static void up_ep_rx_session_rep_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_SESSION_REP_RESP, OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED);
}

static void up_endpoint_rx_cb(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m, struct osmo_pfcp_msg *req)
{
	struct up_endpoint *up_ep = osmo_pfcp_endpoint_get_priv(ep);

	switch (m->h.message_type) {
	case OSMO_PFCP_MSGT_PFD_MGMT_REQ:
		up_ep_rx_pfd_mgmt_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_ASSOC_SETUP_REQ:
		up_ep_rx_assoc_setup_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_ASSOC_UPDATE_REQ:
		up_ep_rx_assoc_upd_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_ASSOC_RELEASE_REQ:
		up_ep_rx_assoc_rel_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_NODE_REPORT_REQ:
		up_ep_rx_node_report_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_SESSION_SET_DEL_REQ:
		up_ep_rx_session_set_del_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_SESSION_EST_REQ:
		up_ep_rx_session_est_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_SESSION_MOD_REQ:
		up_ep_rx_session_mod_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_SESSION_DEL_REQ:
		up_ep_rx_session_del_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_SESSION_REP_REQ:
		up_ep_rx_session_rep_req(up_ep, m);
		return;
	case OSMO_PFCP_MSGT_HEARTBEAT_REQ:
	case OSMO_PFCP_MSGT_HEARTBEAT_RESP:
		/* Heartbeat is already handled in osmo_pfcp_endpoint_handle_rx() in pfcp_endpoint.c. The heartbeat
		 * messages are also dispatched here, to the rx_cb, "on informtional basis", nothing needs to happen
		 * here. */
		return;
	default:
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Unknown message type\n");
		return;
	}
}

struct up_endpoint *up_endpoint_alloc(void *ctx, const struct osmo_sockaddr *local_addr)
{
	struct osmo_pfcp_endpoint_cfg cfg;
	struct up_endpoint *up_ep;
	up_ep = talloc_zero(ctx, struct up_endpoint);
	INIT_LLIST_HEAD(&up_ep->pfcp_node_peer_list);
	hash_init(up_ep->sessions_by_up_seid);

	cfg = (struct osmo_pfcp_endpoint_cfg){
		.local_addr = *local_addr,
		.set_msg_ctx_cb = up_endpoint_set_msg_ctx,
		.rx_msg_cb = up_endpoint_rx_cb,
		.priv = up_ep,
	};
	osmo_pfcp_ie_node_id_from_osmo_sockaddr(&cfg.local_node_id, local_addr);

	up_ep->pfcp_ep = osmo_pfcp_endpoint_create(up_ep, &cfg);
	OSMO_ASSERT(up_ep->pfcp_ep);

	return up_ep;
}

int up_endpoint_bind(struct up_endpoint *up_ep)
{
	OSMO_ASSERT(up_ep);
	OSMO_ASSERT(up_ep->pfcp_ep);
	return osmo_pfcp_endpoint_bind(up_ep->pfcp_ep);
}

struct pfcp_node_peer *up_endpoint_find_pfcp_node_peer(const struct up_endpoint *up_ep,
						       const struct osmo_pfcp_ie_node_id *node_id)
{
	struct pfcp_node_peer *node_peer;
	llist_for_each_entry(node_peer, &up_ep->pfcp_node_peer_list, entry) {
		if (osmo_pfcp_ie_node_id_cmp(&node_peer->node_id, node_id))
			continue;
		return node_peer;
	}
	return NULL;
}

static struct up_session *up_endpoint_find_session(struct up_endpoint *ep, uint64_t up_seid)
{
		struct up_session *session;
		hash_for_each_possible(ep->sessions_by_up_seid, session, ep_node_by_up_seid, up_seid) {
			if (session->up_seid == up_seid)
				return session;
		}
		return NULL;
}

uint64_t up_endpoint_next_up_seid(struct up_endpoint *ep)
{
	uint64_t sanity;
	for (sanity = 2342; sanity; sanity--) {
		uint64_t next_seid = osmo_pfcp_next_seid(&ep->next_up_seid_state);
		if (up_endpoint_find_session(ep, next_seid))
			continue;
		return next_seid;
	}
	return 0;
}

void up_endpoint_free(struct up_endpoint **_ep)
{
	struct pfcp_node_peer *node_peer;
	struct up_endpoint *ep = *_ep;

	while ((node_peer = llist_first_entry_or_null(&ep->pfcp_node_peer_list, struct pfcp_node_peer, entry)))
		pfcp_node_peer_free(node_peer);

	osmo_pfcp_endpoint_free(&ep->pfcp_ep);
	*_ep = NULL;
}
