/*
 * (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/up_endpoint.h>
#include <osmocom/upf/up_peer.h>
#include <osmocom/upf/up_session.h>

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 up_peer *peer;

	/* 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)
			up_peer_set_msg_ctx(req->ctx.peer_fi->priv, m);
		if (!m->ctx.session_fi && req->ctx.session_fi)
			up_session_set_msg_ctx(req->ctx.peer_fi->priv, m);
	}

	/* From the remote address, find the matching peer instance */
	if (!m->ctx.peer_fi) {
		peer = up_peer_find(up_ep, &m->remote_addr);
		if (peer) {
			up_peer_set_msg_ctx(peer, m);
		}
	} else {
		peer = m->ctx.peer_fi->priv;
	}

	/* Find a session, if the header is parsed yet and contains a SEID */
	if (peer && !m->ctx.session_fi && m->h.seid_present) {
		struct up_session *session;
		session = up_session_find_by_up_seid(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, const struct osmo_pfcp_msg *m)
{
	struct up_peer *peer = m->ctx.peer_fi ? m->ctx.peer_fi->priv : NULL;
	if (!peer) {
		peer = up_peer_find_or_add(up_ep, &m->remote_addr);
		OSMO_ASSERT(peer);
	}
	osmo_fsm_inst_dispatch(peer->fi, UP_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, UP_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, UP_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);
}

static void up_ep_rx_session_est_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 establish session\n");
		tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_SESSION_EST_RESP);
		tx->ies.session_est_resp.cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC;
		osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx);
		return;
	}
	osmo_fsm_inst_dispatch(m->ctx.peer_fi, UP_PEER_EV_RX_SESSION_EST_REQ, (void *)m);
}

static void up_ep_rx_session_mod_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	if (!m->ctx.session_fi) {
		/* Session not found. */
		struct osmo_pfcp_msg *tx;
		tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_SESSION_MOD_RESP);
		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");
			tx->ies.session_mod_resp.cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC;
		} else {
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR,
					  "No established session with SEID=0x%"PRIx64", cannot modify\n",
					  m->h.seid);
			tx->ies.session_mod_resp.cause = OSMO_PFCP_CAUSE_SESSION_CTX_NOT_FOUND;
		}
		osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx);
		return;
	}
	osmo_fsm_inst_dispatch(m->ctx.session_fi, UP_SESSION_EV_RX_SESSION_MOD_REQ, (void *)m);
}

static void up_ep_rx_session_del_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m)
{
	if (!m->ctx.session_fi) {
		/* Session not found. */
		struct osmo_pfcp_msg *tx;
		tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, m, OSMO_PFCP_MSGT_SESSION_DEL_RESP);
		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");
			tx->ies.session_del_resp.cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC;
		} else {
			OSMO_LOG_PFCP_MSG(m, LOGL_ERROR,
					  "No established session with SEID=0x%"PRIx64", cannot delete\n",
					  m->h.seid);
			tx->ies.session_del_resp.cause = OSMO_PFCP_CAUSE_SESSION_CTX_NOT_FOUND;
		}
		osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx);
		return;
	}
	osmo_fsm_inst_dispatch(m->ctx.session_fi, UP_SESSION_EV_RX_SESSION_DEL_REQ, (void *)m);
}

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;
	default:
		OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Unknown message type\n");
		return;
	}
}

struct up_endpoint *up_endpoint_init(void *ctx, const struct osmo_sockaddr *local_addr)
{
	int rc;
	struct osmo_pfcp_endpoint_cfg cfg;
	struct up_endpoint *up_ep;
	up_ep = talloc_zero(ctx, struct up_endpoint);
	INIT_LLIST_HEAD(&up_ep->peers);

	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);

	rc = osmo_pfcp_endpoint_bind(up_ep->pfcp_ep);
	if (rc) {
		talloc_free(up_ep);
		return NULL;
	}
	return up_ep;
}

static struct up_session *up_endpoint_find_session(struct up_endpoint *ep, uint64_t up_seid)
{
		struct up_peer *peer;
		llist_for_each_entry(peer, &ep->peers, entry) {
			struct up_session *session = up_session_find_by_up_seid(peer, up_seid);
			if (session)
				return session;
		}
		return NULL;
}

static struct up_session *up_endpoint_find_session_by_local_teid(struct up_endpoint *ep, uint32_t teid)
{
		struct up_peer *peer;
		llist_for_each_entry(peer, &ep->peers, entry) {
			struct up_session *session = up_session_find_by_local_teid(peer, teid);
			if (session)
				return session;
		}
		return NULL;
}

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

static uint32_t up_endpoint_inc_teid(struct up_endpoint *ep)
{
	ep->next_teid_state++;
	if (!ep->next_teid_state)
		ep->next_teid_state++;
	return ep->next_teid_state;
}

uint32_t up_endpoint_next_teid(struct up_endpoint *ep)
{
	uint32_t sanity;
	for (sanity = 2342; sanity; sanity--) {
		uint32_t next_teid = up_endpoint_inc_teid(ep);
		if (up_endpoint_find_session_by_local_teid(ep, next_teid))
			continue;
		return next_teid;
	}
	return 0;
}

void up_endpoint_free(struct up_endpoint **_ep)
{
	struct up_peer *peer;
	struct up_endpoint *ep = *_ep;

	while ((peer = llist_first_entry_or_null(&ep->peers, struct up_peer, entry)))
		up_peer_free(peer);

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