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

enum up_peer_fsm_state {
	UP_PEER_ST_NOT_ASSOCIATED,
	UP_PEER_ST_ASSOCIATED,
	UP_PEER_ST_GRACEFUL_RELEASE,
	UP_PEER_ST_WAIT_USE_COUNT,
};

static const struct value_string up_peer_fsm_event_names[] = {
	OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_SETUP_REQ),
	OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_UPD_REQ),
	OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_REL_REQ),
	OSMO_VALUE_STRING(UP_PEER_EV_RX_SESSION_EST_REQ),
	OSMO_VALUE_STRING(UP_PEER_EV_HEARTBEAT_FAILURE),
	OSMO_VALUE_STRING(UP_PEER_EV_USE_COUNT_ZERO),
	OSMO_VALUE_STRING(UP_PEER_EV_SESSION_TERM),
	{}
};

static struct osmo_fsm up_peer_fsm;

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

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

static int up_peer_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line)
{
	struct up_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, UP_PEER_EV_USE_COUNT_ZERO, NULL);
	return 0;
}

char *up_peer_remote_addr_str(struct up_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 up_peer_update_id(struct up_peer *peer)
{
	osmo_fsm_inst_update_id_f_sanitize(peer->fi, '-', "%s", up_peer_remote_addr_str(peer));
	LOGPFSML(peer->fi, LOGL_DEBUG, "Updated id\n");
}

static struct up_peer *up_peer_add(struct up_endpoint *up_endpoint, const struct osmo_sockaddr *remote_addr)
{
	struct up_peer *peer;

	struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&up_peer_fsm, up_endpoint, NULL, LOGL_DEBUG, NULL);
	OSMO_ASSERT(fi);

	peer = talloc(fi, struct up_peer);
	OSMO_ASSERT(peer);
	fi->priv = peer;

	*peer = (struct up_peer) {
		.fi = fi,
		.up_endpoint = up_endpoint,
		.remote_addr = *remote_addr,
		.heartbeat_fi = NULL /* FIXME */,
		.use_count = {
			.talloc_object = peer,
			.use_cb = up_peer_use_cb,
		},
	};
	osmo_use_count_make_static_entries(&peer->use_count, peer->use_count_buf, ARRAY_SIZE(peer->use_count_buf));
	hash_init(peer->sessions_by_up_seid);
	hash_init(peer->sessions_by_cp_seid);

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

	up_peer_update_id(peer);

	llist_add(&peer->entry, &up_endpoint->peers);
	return peer;
}

struct up_peer *up_peer_find(struct up_endpoint *up_endpoint, const struct osmo_sockaddr *remote_addr)
{
	struct up_peer *peer;
	llist_for_each_entry(peer, &up_endpoint->peers, entry) {
		if (osmo_sockaddr_cmp(&peer->remote_addr, remote_addr))
			continue;
		return peer;
	}
	return NULL;
}

struct up_peer *up_peer_find_or_add(struct up_endpoint *up_endpoint, const struct osmo_sockaddr *remote_addr)
{
	struct up_peer *peer = up_peer_find(up_endpoint, remote_addr);
	if (peer)
		return peer;
	return up_peer_add(up_endpoint, remote_addr);
}

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

void up_peer_set_msg_ctx(struct up_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);
}

struct osmo_pfcp_msg *up_peer_init_tx(struct up_peer *peer, struct osmo_pfcp_msg *in_reply_to,
				      enum osmo_pfcp_message_type message_type)
{
	struct osmo_pfcp_msg *tx;
	if (in_reply_to)
		tx = osmo_pfcp_msg_alloc_tx_resp(OTC_SELECT, in_reply_to, message_type);
	else
		tx = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, message_type);
	up_peer_set_msg_ctx(peer, tx);
	return tx;
}

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

	resp = up_peer_init_tx(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 = peer->local_up_features,
	};

	if (osmo_pfcp_endpoint_tx(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 up_peer_tx_assoc_rel_resp(struct up_peer *peer, struct osmo_pfcp_msg *m, enum osmo_pfcp_cause cause)
{
	struct osmo_pfcp_msg *resp;

	resp = up_peer_init_tx(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->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 up_peer_clear_sessions(struct up_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 up_peer_rx_assoc_setup_req(struct up_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 == UP_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");
			up_peer_clear_sessions(peer);
		} else {
			LOGPFSML(fi, LOGL_NOTICE, "another Association Setup Request, with same Recovery Timestamp."
				 " Keeping sessions, sending ACK.\n");
		}
	} else if (up_peer_fsm_state_chg(UP_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;
		peer->remote_node_id = m->ies.assoc_setup_req.node_id;
		if (m->ies.assoc_setup_req.cp_function_features_present)
			peer->peer_cp_features = m->ies.assoc_setup_req.cp_function_features;
	}

	if (up_peer_tx_assoc_setup_resp(peer, m, cause)
	    || cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
		up_peer_fsm_state_chg(UP_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->remote_node_id),
		 osmo_pfcp_bits_to_str_c(OTC_SELECT, peer->local_up_features.bits, osmo_pfcp_up_feature_strs),
		 osmo_pfcp_bits_to_str_c(OTC_SELECT, peer->peer_cp_features.bits, osmo_pfcp_cp_feature_strs));
}

static void up_peer_rx_assoc_rel_req(struct up_peer *peer, struct osmo_pfcp_msg *m)
{
	struct osmo_fsm_inst *fi = peer->fi;
	up_peer_tx_assoc_rel_resp(peer, m, OSMO_PFCP_CAUSE_REQUEST_ACCEPTED);
	up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT);
}

static void up_peer_rx_session_est_req(struct up_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 = up_session_find_or_add(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 = up_peer_init_tx(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->up_endpoint->pfcp_ep, resp);
	if (session)
		up_session_discard(session);
}

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

	switch (event) {

	case UP_PEER_EV_RX_ASSOC_SETUP_REQ:
		up_peer_rx_assoc_setup_req(peer, data);
		break;

	case UP_PEER_EV_USE_COUNT_ZERO:
		/* Not associated and no pending messages. discard peer. */
		up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT);
		return;

	default:
		OSMO_ASSERT(false);
	}
}

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

	switch (event) {

	case UP_PEER_EV_RX_ASSOC_SETUP_REQ:
		up_peer_rx_assoc_setup_req(peer, data);
		break;

	case UP_PEER_EV_RX_ASSOC_UPD_REQ:
		// FIXME
		break;

	case UP_PEER_EV_RX_SESSION_EST_REQ:
		up_peer_rx_session_est_req(peer, data);
		break;

	case UP_PEER_EV_HEARTBEAT_FAILURE:
		// FIXME
		break;

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

	default:
		OSMO_ASSERT(false);
	}
}

static void up_peer_associated_onleave(struct osmo_fsm_inst *fi, uint32_t next_state)
{
	struct up_peer *peer = fi->priv;
	if (next_state != UP_PEER_ST_ASSOCIATED)
		LOGPFSML(fi, LOGL_NOTICE, "Peer %s released\n",
			 osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &peer->remote_node_id));
}

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

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

	switch (event) {

	case UP_PEER_EV_HEARTBEAT_FAILURE:
		up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT);
		break;

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

	default:
		OSMO_ASSERT(false);
	}
}

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

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

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

	case UP_PEER_EV_RX_ASSOC_SETUP_REQ:
		up_peer_rx_assoc_setup_req(peer, data);
		break;

	default:
		OSMO_ASSERT(false);
	}
}

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

static const struct osmo_fsm_state up_peer_fsm_states[] = {
	[UP_PEER_ST_NOT_ASSOCIATED] = {
		.name = "NOT_ASSOCIATED",
		.in_event_mask = 0
			| S(UP_PEER_EV_RX_ASSOC_SETUP_REQ)
			| S(UP_PEER_EV_USE_COUNT_ZERO)
			,
		.out_state_mask = 0
			| S(UP_PEER_ST_ASSOCIATED)
			| S(UP_PEER_ST_WAIT_USE_COUNT)
			,
		.action = up_peer_not_associated_action,
	},
	[UP_PEER_ST_ASSOCIATED] = {
		.name = "ASSOCIATED",
		.in_event_mask = 0
			| S(UP_PEER_EV_RX_ASSOC_SETUP_REQ)
			| S(UP_PEER_EV_RX_ASSOC_UPD_REQ)
			| S(UP_PEER_EV_RX_SESSION_EST_REQ)
			| S(UP_PEER_EV_HEARTBEAT_FAILURE)
			| S(UP_PEER_EV_USE_COUNT_ZERO)
			,
		.out_state_mask = 0
			| S(UP_PEER_ST_ASSOCIATED)
			| S(UP_PEER_ST_GRACEFUL_RELEASE)
			| S(UP_PEER_ST_WAIT_USE_COUNT)
			,
		.action = up_peer_associated_action,
		.onleave = up_peer_associated_onleave,
	},
	[UP_PEER_ST_GRACEFUL_RELEASE] = {
		.name = "GRACEFUL_RELEASE",
		.in_event_mask = 0
			| S(UP_PEER_EV_HEARTBEAT_FAILURE)
			| S(UP_PEER_EV_USE_COUNT_ZERO)
			,
		.out_state_mask = 0
			| S(UP_PEER_ST_WAIT_USE_COUNT)
			,
		.onenter = up_peer_graceful_release_onenter,
		.action = up_peer_graceful_release_action,
	},
	[UP_PEER_ST_WAIT_USE_COUNT] = {
		.name = "WAIT_USE_COUNT",
		.in_event_mask = 0
			| S(UP_PEER_EV_USE_COUNT_ZERO)
			| S(UP_PEER_EV_RX_ASSOC_SETUP_REQ)
			,
		.out_state_mask = 0
			| S(UP_PEER_ST_ASSOCIATED)
			,
		.onenter = up_peer_wait_use_count_onenter,
		.action = up_peer_wait_use_count_action,
	},
};

void up_peer_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
{
	struct up_peer *peer = fi->priv;
	LOGPFSML(fi, LOGL_NOTICE, "Peer removed\n");
	llist_del(&peer->entry);
}

static void up_peer_allstate_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	switch (event) {
	case UP_PEER_EV_SESSION_TERM:
		/* ignore */
		return;
	case UP_PEER_EV_RX_ASSOC_REL_REQ:
		up_peer_rx_assoc_rel_req(fi->priv, data);
		return;
	default:
		OSMO_ASSERT(false);
	}
}

static struct osmo_fsm up_peer_fsm = {
	.name = "up_peer",
	.log_subsys = DPEER,
	.states = up_peer_fsm_states,
	.num_states = ARRAY_SIZE(up_peer_fsm_states),
	.event_names = up_peer_fsm_event_names,
	.timer_cb = up_peer_fsm_timer_cb,
	.cleanup = up_peer_fsm_cleanup,
	.allstate_event_mask = 0
		| S(UP_PEER_EV_RX_ASSOC_REL_REQ)
		| S(UP_PEER_EV_SESSION_TERM)
		,
	.allstate_action = up_peer_allstate_action,
};

static __attribute__((constructor)) void up_peer_fsm_register(void)
{
	OSMO_ASSERT(osmo_fsm_register(&up_peer_fsm) == 0);
}

void up_peer_free(struct up_peer *peer)
{
	osmo_fsm_inst_term(peer->fi, OSMO_FSM_TERM_REGULAR, NULL);
}
