/* (C) 2017 by sysmocom s.f.m.c. GmbH
 * All Rights Reserved
 *
 * Author: Philipp Maier
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <osmocom/core/utils.h>
#include <osmocom/core/logging.h>
#include <osmocom/sigtran/osmo_ss7.h>
#include <osmocom/sigtran/sccp_sap.h>
#include <osmocom/sccp/sccp_types.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/gsm/gsm0808.h>
#include <osmocom/core/msgb.h>
#include <openbsc/bsc_msc_data.h>
#include <openbsc/debug.h>
#include <openbsc/osmo_bsc.h>
#include <openbsc/osmo_bsc_grace.h>
#include <openbsc/osmo_bsc_sigtran.h>
#include <openbsc/a_reset.h>
#include <openbsc/gsm_04_80.h>

/* A pointer to a list with all involved MSCs
 * (a copy of the pointer location submitted with osmo_bsc_sigtran_init() */
static struct llist_head *msc_list;

#define RESET_INTERVAL 1	/* sek */
#define SCCP_MSG_MAXSIZE 1024

/* Internal list with connections we currently maintain. This
 * list is of type struct osmo_bsc_sccp_con */
static LLIST_HEAD(active_connections);

/* The SCCP stack will not assign connection IDs to us automatically, we
 * will do this ourselves using a counter variable, that counts one up
 * for every new connection */
static uint32_t conn_id_counter;

/* Helper function to Check if the given connection id is already assigned */
static struct osmo_bsc_sccp_con *get_bsc_conn_by_conn_id(int conn_id)
{
	conn_id &= 0xFFFFFF;
	struct osmo_bsc_sccp_con *bsc_con;

	llist_for_each_entry(bsc_con, &active_connections, entry) {
		if (bsc_con->conn_id == conn_id)
			return bsc_con;
	}

	return NULL;
}

/* Pick a free connection id */
static int pick_free_conn_id(const struct bsc_msc_data *msc)
{
	int conn_id = conn_id_counter;
	int i;

	for (i = 0; i < 0xFFFFFF; i++) {
		conn_id++;
		conn_id &= 0xFFFFFF;
		if (get_bsc_conn_by_conn_id(conn_id) == false) {
			conn_id_counter = conn_id;
			return conn_id;
		}
	}

	return -1;
}

/* Send reset to MSC */
static void osmo_bsc_sigtran_tx_reset(const struct bsc_msc_data *msc)
{
	struct msgb *msg;
	LOGP(DMSC, LOGL_NOTICE, "Sending RESET to MSC: %s\n", osmo_sccp_addr_dump(&msc->a.msc_addr));
	msg = gsm0808_create_reset();
	osmo_sccp_tx_unitdata_msg(msc->a.sccp_user, &msc->a.bsc_addr,
				  &msc->a.msc_addr, msg);
}

/* Send reset-ack to MSC */
void osmo_bsc_sigtran_tx_reset_ack(const struct bsc_msc_data *msc)
{
	struct msgb *msg;
	OSMO_ASSERT(msc);
	LOGP(DMSC, LOGL_NOTICE, "Sending RESET ACK to MSC: %s\n", osmo_sccp_addr_dump(&msc->a.msc_addr));
	msg = gsm0808_create_reset_ack();
	osmo_sccp_tx_unitdata_msg(msc->a.sccp_user, &msc->a.bsc_addr,
				  &msc->a.msc_addr, msg);
}

/* Find an MSC by its sigtran point code */
static struct bsc_msc_data *get_msc_by_addr(const struct osmo_sccp_addr *msc_addr)
{
	struct bsc_msc_data *msc;
	llist_for_each_entry(msc, msc_list, entry) {
		if (memcmp(msc_addr, &msc->a.msc_addr, sizeof(*msc_addr)) == 0)
			return msc;
	}

	LOGP(DMSC, LOGL_ERROR, "Unable to find MSC data under address: %s\n", osmo_sccp_addr_dump(msc_addr));
	return NULL;
}

/* Send data to MSC, use the connection id which MSC it is */
static int handle_data_from_msc(int conn_id, struct msgb *msg)
{
	struct osmo_bsc_sccp_con *bsc_con = get_bsc_conn_by_conn_id(conn_id);
	int rc = -EINVAL;

	if (bsc_con) {
		msg->l3h = msgb_l2(msg);
		rc = bsc_handle_dt(bsc_con, msg, msgb_l2len(msg));
	} else
		LOGP(DMSC, LOGL_NOTICE, "incoming data from unknown connection id: %i\n", conn_id);

	return rc;
}

/* Sent unitdata to MSC, use the point code to determine which MSC it is */
static int handle_unitdata_from_msc(const struct osmo_sccp_addr *msc_addr, struct msgb *msg)
{
	struct bsc_msc_data *msc = get_msc_by_addr(msc_addr);
	int rc = -EINVAL;

	if (msc) {
		msg->l3h = msgb_l2(msg);
		rc = bsc_handle_udt(msc, msg, msgb_l2len(msg));
	} else
		LOGP(DMSC, LOGL_NOTICE, "incoming unitdata data from unknown remote address: %s\n",
		     osmo_sccp_addr_dump(msc_addr));

	return rc;
}

/* Callback function, called by the SSCP stack when data arrives */
static int sccp_sap_up(struct osmo_prim_hdr *oph, void *_scu)
{
	struct osmo_scu_prim *scu_prim = (struct osmo_scu_prim *)oph;
	struct osmo_sccp_user *scu = _scu;
	struct osmo_bsc_sccp_con *bsc_con;
	int rc = 0;

	switch (OSMO_PRIM_HDR(&scu_prim->oph)) {
	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_INDICATION):
		/* Handle inbound UNITDATA */
		DEBUGP(DMSC, "N-UNITDATA.ind(%s)\n", osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
		rc = handle_unitdata_from_msc(&scu_prim->u.unitdata.calling_addr, oph->msg);
		break;

	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION):
		/* Handle (Reject) inbound connections */
		DEBUGP(DMSC, "N-CONNECT.ind(X->%u)\n", scu_prim->u.connect.conn_id);
		LOGP(DMSC, LOGL_DEBUG, "Rejecting inbound SCCP connection...\n");
		rc = osmo_sccp_tx_disconn(scu, scu_prim->u.connect.conn_id, &scu_prim->u.connect.called_addr, 0);
		break;

	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM):
		/* Handle outbound connection confirmation */
		if (msgb_l2len(oph->msg) > 0) {
			DEBUGP(DMSC, "N-CONNECT.cnf(%u, %s)\n", scu_prim->u.connect.conn_id,
			       osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
			rc = handle_data_from_msc(scu_prim->u.connect.conn_id, oph->msg);
		} else
			DEBUGP(DRANAP, "N-CONNECT.cnf(%u)\n", scu_prim->u.connect.conn_id);
		break;

	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION):
		/* Handle incoming connection oriented data */
		DEBUGP(DMSC, "N-DATA.ind(%u, %s)\n", scu_prim->u.data.conn_id,
		       osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));

		/* Incoming data is a sign of a vital connection */
		bsc_con = get_bsc_conn_by_conn_id(scu_prim->u.disconnect.conn_id);
		if (bsc_con)
			a_reset_conn_success(bsc_con->msc->a.reset);

		rc = handle_data_from_msc(scu_prim->u.data.conn_id, oph->msg);
		break;

	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION):
		/* indication of disconnect */
		if (msgb_l2len(oph->msg) > 0) {
			DEBUGP(DMSC, "N-DISCONNECT.ind(%u, %s, cause=%i)\n", scu_prim->u.disconnect.conn_id,
			       osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)), scu_prim->u.disconnect.cause);
			handle_data_from_msc(scu_prim->u.disconnect.conn_id, oph->msg);
		} else
			DEBUGP(DRANAP, "N-DISCONNECT.ind(%u, cause=%i)\n", scu_prim->u.disconnect.conn_id,
			       scu_prim->u.disconnect.cause);

		bsc_con = get_bsc_conn_by_conn_id(scu_prim->u.disconnect.conn_id);
		if (bsc_con) {
			/* We might have a connectivity problem. Maybe we need to go
			 * through the reset procedure again? */
			if (scu_prim->u.disconnect.cause == 0)
				a_reset_conn_fail(bsc_con->msc->a.reset);

			rc = osmo_bsc_sigtran_del_conn(bsc_con);
		}
		break;

	default:
		LOGP(DMSC, LOGL_ERROR, "Unhandled SIGTRAN primitive: %u:%u\n", oph->primitive, oph->operation);
		break;
	}

	msgb_free(oph->msg);
	return rc;
}

/* Allocate resources to make a new connection oriented sigtran connection
 * (not the connection ittself!) */
enum bsc_con osmo_bsc_sigtran_new_conn(struct gsm_subscriber_connection *conn, struct bsc_msc_data *msc)
{
	struct osmo_bsc_sccp_con *bsc_con;
	int conn_id;

	OSMO_ASSERT(conn);
	OSMO_ASSERT(msc);

	LOGP(DMSC, LOGL_NOTICE, "Initializing resources for new SIGTRAN connection to MSC: %s...\n",
	     osmo_sccp_addr_dump(&msc->a.msc_addr));

	if (a_reset_conn_ready(msc->a.reset) == false) {
		LOGP(DMSC, LOGL_ERROR, "MSC is not connected. Dropping.\n");
		return BSC_CON_REJECT_NO_LINK;
	}

	if (!bsc_grace_allow_new_connection(conn->bts->network, conn->bts)) {
		LOGP(DMSC, LOGL_NOTICE, "BSC in grace period. No new connections.\n");
		return BSC_CON_REJECT_RF_GRACE;
	}

	bsc_con = talloc_zero(conn->bts, struct osmo_bsc_sccp_con);
	if (!bsc_con) {
		LOGP(DMSC, LOGL_ERROR, "Failed to allocate new SIGTRAN connection.\n");
		return BSC_CON_NO_MEM;
	}

	bsc_con->msc = msc;
	bsc_con->conn = conn;
	llist_add_tail(&bsc_con->entry, &active_connections);
	conn->sccp_con = bsc_con;

	/* Pick a free connection id */
	conn_id = pick_free_conn_id(msc);
	if (conn_id < 0)
		return BSC_CON_REJECT_NO_LINK;
	bsc_con->conn_id = conn_id;

	LOGP(DMSC, LOGL_NOTICE, "Allocated new connection id: %i\n", conn_id);

	return BSC_CON_SUCCESS;
}

/* Open a new connection oriented sigtran connection */
int osmo_bsc_sigtran_open_conn(const struct osmo_bsc_sccp_con *conn, struct msgb *msg)
{
	struct bsc_msc_data *msc;
	int conn_id;
	int rc;

	OSMO_ASSERT(conn);
	OSMO_ASSERT(msg);
	OSMO_ASSERT(conn->msc);

	msc = conn->msc;

	if (a_reset_conn_ready(msc->a.reset) == false) {
		LOGP(DMSC, LOGL_ERROR, "MSC is not connected. Dropping.\n");
		return -EINVAL;
	}

	conn_id = conn->conn_id;
	LOGP(DMSC, LOGL_NOTICE, "Opening new SIGTRAN connection (id=%i) to MSC: %s\n", conn_id,
	     osmo_sccp_addr_dump(&msc->a.msc_addr));

	rc = osmo_sccp_tx_conn_req_msg(msc->a.sccp_user, conn_id, &msc->a.bsc_addr,
				       &msc->a.msc_addr, msg);

	return rc;
}

/* Send data to MSC */
int osmo_bsc_sigtran_send(const struct osmo_bsc_sccp_con *conn, struct msgb *msg)
{
	int conn_id;
	int rc;
	struct bsc_msc_data *msc;

	OSMO_ASSERT(conn);
	OSMO_ASSERT(msg);
	OSMO_ASSERT(conn->msc);

	msc = conn->msc;

	if (a_reset_conn_ready(msc->a.reset) == false) {
		LOGP(DMSC, LOGL_ERROR, "MSC is not connected. Dropping.\n");
		return -EINVAL;
	}

	conn_id = conn->conn_id;

	LOGP(DMSC, LOGL_DEBUG, "Sending connection (id=%i) oriented data to MSC: %si\n",
	     conn_id, osmo_sccp_addr_dump(&msc->a.msc_addr));

	rc = osmo_sccp_tx_data_msg(msc->a.sccp_user, conn_id, msg);

	return rc;
}

/* Delete a connection from the list with open connections
 * (called by osmo_bsc_api.c on failing open connections and
 * locally, when a connection is closed by the MSC */
int osmo_bsc_sigtran_del_conn(struct osmo_bsc_sccp_con *conn)
{
	if (!conn)
		return 0;

	if (conn->conn) {
		LOGP(DMSC, LOGL_ERROR,
		     "sccp connection (id=%i) not cleared (gsm subscriber connection still active) -- forcefully clearing it now!\n",
		     conn->conn_id);
		bsc_subscr_con_free(conn->conn);
		conn->conn = NULL;

		/* This bahaviour might be caused by a bad connection. Maybe we
		 * will have to go through the reset procedure again */
		a_reset_conn_fail(conn->msc->a.reset);
	}

	llist_del(&conn->entry);
	talloc_free(conn);

	return 0;
}

/* Send an USSD notification in case we loose the connection to the MSC */
static void bsc_notify_msc_lost(const struct osmo_bsc_sccp_con *con)
{
	struct gsm_subscriber_connection *conn = con->conn;

	/* send USSD notification if string configured and con->data is set */
	if (!conn)
		return;

	/* check for config string */
	if (!con->msc->ussd_msc_lost_txt)
		return;
	if (con->msc->ussd_msc_lost_txt[0] == '\0')
		return;

	/* send USSD notification */
	bsc_send_ussd_notify(conn, 1, conn->sccp_con->msc->ussd_msc_lost_txt);
	bsc_send_ussd_release_complete(conn);
}

/* Close all open sigtran connections and channels */
void osmo_bsc_sigtran_reset(const struct bsc_msc_data *msc)
{
	struct osmo_bsc_sccp_con *conn;
	struct osmo_bsc_sccp_con *conn_temp;
	OSMO_ASSERT(msc);

	/* Close all open connections */
	llist_for_each_entry_safe(conn, conn_temp, &active_connections, entry) {

		/* We only may close connections which actually belong to this
		 * MSC. All other open connections are left untouched */
		if (conn->msc == msc) {
			/* Notify active connection users via USSD that the MSC is down */
			bsc_notify_msc_lost(conn);

			/* Take down all occopied RF channels */
			if (conn->conn)
				gsm0808_clear(conn->conn);

			/* Disconnect all Sigtran connections */
			osmo_sccp_tx_disconn(msc->a.sccp_user, conn->conn_id, &msc->a.bsc_addr, 0);

			/* Delete subscriber connection */
			osmo_bsc_sigtran_del_conn(conn);
		}
	}
}

/* Callback function: Close all open connections */
static void osmo_bsc_sigtran_reset_cb(const void *priv)
{
	struct bsc_msc_data *msc = (struct bsc_msc_data*) priv;

	/* Shut down all ongoing traffic */
	osmo_bsc_sigtran_reset(msc);

	/* Send reset to MSC */
	osmo_bsc_sigtran_tx_reset(msc);
}


/* Initalize osmo sigtran backhaul */
int osmo_bsc_sigtran_init(struct llist_head *mscs)
{
	/* FIXME: Remove hardcoded IP-Addresses */
	struct bsc_msc_data *msc;
	char msc_name[256];

	OSMO_ASSERT(mscs);

	msc_list = mscs;

	conn_id_counter = 0;

	llist_for_each_entry(msc, msc_list, entry) {
		snprintf(msc_name, sizeof(msc_name), "msc-%u", msc->nr);
		LOGP(DMSC, LOGL_NOTICE, "Initializing SCCP connection to MSC %s (%s)\n",
		     osmo_sccp_addr_dump(&msc->a.msc_addr), msc_name);

		/* Check if the VTY could determine a valid CS7 instance */
		if (msc->a.cs7_instance_valid == false) {
			LOGP(DMSC, LOGL_ERROR, "A-interface: Invalid CS7 instance\n");
			return -EINVAL;
		}

		/* Check if the sccp-address fullfill minimum requirements (SSN+PC is present) */
		if (!osmo_sccp_check_addr(&msc->a.bsc_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) {
			LOGP(DMSC, LOGL_ERROR,
			     "A-interface: invalid local SCCP address (a.bsc_addr=%s)\n",
			     osmo_sccp_addr_dump(&msc->a.bsc_addr));
			return -EINVAL;
		}
		if (!osmo_sccp_check_addr(&msc->a.msc_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) {
			LOGP(DMSC, LOGL_ERROR,
			     "A-interface: invalid remote SCCP address for the MSC (a.msc_addr=%s)\n",
			     osmo_sccp_addr_dump(&msc->a.msc_addr));
			return -EINVAL;
		}

		/* SS7 Protocol stack */
		msc->a.sccp =
		    osmo_sccp_simple_client_on_ss7_id(msc, msc->a.cs7_instance, msc_name, msc->a.bsc_addr.pc,
						      OSMO_SS7_ASP_PROT_M3UA, 0, NULL, M3UA_PORT, "127.0.0.1");
		if (!msc->a.sccp)
			return -EINVAL;

		msc->a.sccp_user = osmo_sccp_user_bind(msc->a.sccp, msc_name, sccp_sap_up, msc->a.bsc_addr.ssn);
		if (!msc->a.sccp_user)
			return -EINVAL;

		/* Start MSC reset procedure */
		msc->a.reset = a_reset_alloc(msc, msc_name, osmo_bsc_sigtran_reset_cb, msc);
		if (!msc->a.reset)
			return -EINVAL;

	}

	return 0;
}
