/* SCCP Routing Control (SCRC) according to ITU-T Q.714 */

/* (C) 2015-2017 by Harald Welte <laforge@gnumonks.org>
 * All Rights reserved
 *
 * 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 <stdbool.h>
#include <errno.h>

#include <osmocom/core/logging.h>
#include <osmocom/core/msgb.h>

#include <osmocom/sccp/sccp_types.h>
#include <osmocom/sigtran/osmo_ss7.h>
#include <osmocom/sigtran/sccp_sap.h>
#include <osmocom/sigtran/sccp_helpers.h>
#include <osmocom/sigtran/protocol/sua.h>
#include <osmocom/sigtran/protocol/mtp.h>

#include "sccp_instance.h"
#include "sccp_internal.h"
#include "ss7_as.h"
#include "ss7_instance.h"
#include "ss7_linkset.h"
#include "ss7_route.h"
#include "ss7_route_table.h"
#include "xua_internal.h"


/***********************************************************************
 * Helper Functions
 ***********************************************************************/

static bool sua_is_connectionless(struct xua_msg *xua)
{
	if (xua->hdr.msg_class == SUA_MSGC_CL)
		return true;
	else
		return false;
}

static bool sua_is_cr(struct xua_msg *xua)
{
	if (xua->hdr.msg_class == SUA_MSGC_CO &&
	    xua->hdr.msg_type == SUA_CO_CORE)
		return true;

	return false;
}

/* ITU Q.714: "DPC accessible ?"
 * Figure C.1/Q.714 - SCCP routing control procedures (SCRC)
 * "sheet 4 of 12", "sheet 5 of 12", "sheet 10 of 12"
 */
static bool dpc_accessible(struct osmo_sccp_instance *inst, uint32_t dpc)
{
	struct osmo_ss7_instance *ss7 = inst->ss7;
	char buf_dpc[MAX_PC_STR_LEN];
	bool found;

	if (osmo_ss7_pc_is_local(ss7, dpc)) {
		LOGPSCI(inst, LOGL_DEBUG, "dpc_accessible(DPC=%u=%s): true (local)\n",
			dpc, osmo_ss7_pointcode_print_buf(buf_dpc, sizeof(buf_dpc), ss7, dpc));
		return true;
	}

	found = ss7_route_table_dpc_is_accessible(ss7->rtable_system, dpc);

	LOGPSCI(inst, found ? LOGL_DEBUG : LOGL_INFO,
		"dpc_accessible(DPC=%u=%s): %s\n",
		dpc, osmo_ss7_pointcode_print_buf(buf_dpc, sizeof(buf_dpc), ss7, dpc),
		found ? "true" : "false");
	return found;

}

static bool sccp_available(struct osmo_sccp_instance *inst,
			   const struct osmo_sccp_addr *addr)
{
	/* TODO: implement this! */
	return true;
}

static int sua2sccp_tx_m3ua(struct osmo_sccp_instance *inst,
			    struct xua_msg *sua)
{
	struct msgb *msg;
	struct osmo_mtp_prim *omp;
	struct osmo_mtp_transfer_param *param;
	struct osmo_ss7_instance *s7i = inst->ss7;
	uint32_t remote_pc = sua->mtp.dpc;

	/* 1) encode the SUA in xua_msg to SCCP message */
	msg = osmo_sua_to_sccp(sua);
	if (!msg) {
		LOGPSCI(inst, LOGL_ERROR, "Cannot encode SUA to SCCP\n");
		return -1;
	}

	/* 2) wrap into MTP-TRANSFER.req primitive */
	omp = osmo_mtp_prim_xfer_req_prepend(NULL, msg);
	param = &omp->u.transfer;
	if (sua->mtp.opc)
		param->opc = sua->mtp.opc;
	else {
		if (!osmo_ss7_pc_is_valid(s7i->cfg.primary_pc)) {
			LOGPSCI(inst, LOGL_ERROR, "SS7 instance %u: no primary point-code set\n",
				s7i->cfg.id);
			return -1;
		}
		param->opc = s7i->cfg.primary_pc;
	}
	param->dpc = remote_pc;
	param->sls = sua->mtp.sls;
	param->sio = MTP_SIO(MTP_SI_SCCP, s7i->cfg.network_indicator);

	/* 3) send via MTP-SAP (osmo_ss7_instance) */
	return osmo_ss7_user_mtp_sap_prim_down(inst->ss7_user, omp);
}

/* Generate MTP-TRANSFER.req from xUA message */
static int gen_mtp_transfer_req_xua(struct osmo_sccp_instance *inst,
				    struct xua_msg *xua,
				    const struct osmo_sccp_addr *called)
{
	struct osmo_sccp_addr calling;
	struct osmo_ss7_route *rt;
	struct osmo_ss7_route_label rtlabel;

	/* this is a bit fishy due to the different requirements of
	 * classic SSCP/MTP compared to various SIGTRAN stackings.
	 * Normally, we would expect a fully encoded SCCP message here,
	 * but then if the route points to a SUA link, we actually need
	 * the SUA version of the message.
	 *
	 * We need to differentiate the following cases:
	 * a) SUA: encode XUA to SUA and send via ASP
	 * b) M3UA: encode XUA to SCCP, create MTP-TRANSFER.req
	 *    primitive and send it via ASP
	 * c) M2UA/M2PA or CS7: encode XUA, create MTP-TRANSFER.req
	 *    primitive and send it via link
	 */

	if (sua_addr_parse(&calling, xua, SUA_IEI_SRC_ADDR) == 0 &&
	    (calling.presence & OSMO_SCCP_ADDR_T_PC))
		xua->mtp.opc = calling.pc;


	if (called->presence & OSMO_SCCP_ADDR_T_PC)
		xua->mtp.dpc = called->pc;

	rtlabel = (struct osmo_ss7_route_label){
		.opc = xua->mtp.opc,
		.dpc = xua->mtp.dpc,
		.sls = xua->mtp.sls,
	};

	rt = ss7_instance_lookup_route(inst->ss7, &rtlabel);
	if (!rt) {
		char buf[256];
		LOGPSCI(inst, LOGL_ERROR, "MTP-TRANSFER.req from SCCP for %s: no route!\n",
			ss7_route_label_to_str(buf, sizeof(buf), inst->ss7, &rtlabel));
		sccp_rout_fail_enqueue(inst, xua, SCCP_RETURN_CAUSE_MTP_FAILURE, xua->hdr.msg_class == SUA_MSGC_CO);
		return -1;
	}

	if (rt->dest.as) {
		struct osmo_ss7_as *as = rt->dest.as;
		switch (as->cfg.proto) {
		case OSMO_SS7_ASP_PROT_SUA:
			return sua_tx_xua_as(as, xua);
		case OSMO_SS7_ASP_PROT_M3UA:
		case OSMO_SS7_ASP_PROT_IPA:
			return sua2sccp_tx_m3ua(inst, xua);
		default:
			LOGPSCI(inst, LOGL_ERROR, "MTP-TRANSFER.req for "
				"unknown protocol %u\n", as->cfg.proto);
			break;
		}
	} else if (rt->dest.linkset) {
		LOGPSCI(inst, LOGL_ERROR, "MTP-TRANSFER.req from SCCP for "
			"linkset %s unsupported\n", rt->dest.linkset->cfg.name);
	} else {
		OSMO_ASSERT(0);
	}
	return -1;
}

/***********************************************************************
 * Global Title Translation
 ***********************************************************************/

static int translate(struct osmo_sccp_instance *inst,
		     const struct osmo_sccp_addr *called,
		     struct osmo_sccp_addr *translated)
{
	/* TODO: implement this! */
	*translated = *called;
	return 0;
}


/***********************************************************************
 * Individual SCRC Nodes
 ***********************************************************************/

static int scrc_local_out_common(struct osmo_sccp_instance *inst,
				 struct xua_msg *xua,
				 const struct osmo_sccp_addr *called);

static int scrc_node_12(struct osmo_sccp_instance *inst, struct xua_msg *xua,
			const struct osmo_sccp_addr *called)
{
	/* TODO: Determine restriction */
	/* TODO: Treat Calling Party Addr */
	/* TODO: Hop counter */
	/* MTP-TRANSFER.req to MTP */
	return gen_mtp_transfer_req_xua(inst, xua, called);
}

/* "Figure C.1/Q.714 - SCCP routing control procedures (SCRC) (sheet 5 of 12)"
 * Only used for Connection Oriented messages */
static int scrc_node_2(struct osmo_sccp_instance *inst, struct xua_msg *xua)
{
	struct osmo_sccp_addr called;
	int rc;

	rc = sua_addr_parse(&called, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		/* Q.714 2.2.2: It is expectd that CO msgs != CREQ have no "Called Address".
		* Use MTP DPC info provided by SCOC in xua->mtp.dpc instead. */
		called = (struct osmo_sccp_addr){
			.ri = OSMO_SCCP_RI_SSN_PC,
			.presence = OSMO_SCCP_ADDR_T_PC,
			.pc = xua->mtp.dpc,
		};
	}
	if (!(called.presence & OSMO_SCCP_ADDR_T_PC)) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid DPC address information\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return -EINVAL;
	}

	/* Is DPC accessible? */
	if (!dpc_accessible(inst, called.pc)) {
		/* Error: MTP Failure */
		/* Routing Failure SCRC -> SCOC */
		sccp_rout_fail_enqueue(inst, xua, SCCP_RETURN_CAUSE_MTP_FAILURE, true);
		return 0;
	}
	/* Is SCCP available? */
	if (!sccp_available(inst, &called)) {
		/* Error: SCCP Failure */
		/* Routing Failure SCRC -> SCOC */
		sccp_rout_fail_enqueue(inst, xua, SCCP_RETURN_CAUSE_SCCP_FAILURE, true);
		return 0;
	}
	return scrc_node_12(inst, xua, &called);
}

static int scrc_node_7(struct osmo_sccp_instance *inst,
			struct xua_msg *xua,
			const struct osmo_sccp_addr *called)
{
	/* Connection Oriented? */
	if (sua_is_connectionless(xua)) {
		/* TODO: Perform Capability Test */
		/* TODO: Changes Needed? */
		if (0) {
			/* Changes Needed -> SCLC */
			return 0;
		}
	} else {
		/* TODO: Coupling Required? */
		if (0) {
			/* Node 13 (Sheet 5) */
		}
	}
	return scrc_node_12(inst, xua, called);
}

/* Node 4 (Sheet 3) */
static int scrc_node_4(struct osmo_sccp_instance *inst,
		       struct xua_msg *xua, uint32_t return_cause)
{
	/* TODO: Routing Failure SCRC -> OMAP */
	if (sua_is_connectionless(xua)) {
		/* Routing Failure SCRC -> SCLC */
		sccp_rout_fail_enqueue(inst, xua, return_cause, false);
	} else {
		/* Routing Failure SCRC -> SCOC */
		sccp_rout_fail_enqueue(inst, xua, return_cause, true);
	}
	return 0;
}

static int scrc_translate_node_9(struct osmo_sccp_instance *inst,
				 struct xua_msg *xua,
				 const struct osmo_sccp_addr *called)
{
	struct osmo_sccp_addr translated;
	int rc;

	/* Translate */
	rc = translate(inst, called, &translated);
	/* Node 9 (Sheet 3) */
	if (rc < 0) {
		/* Node 4 (Sheet 3) */
		return scrc_node_4(inst, xua,
				   SCCP_RETURN_CAUSE_NO_TRANSLATION);
	}
	/* Route on SSN? */
	if (translated.ri != OSMO_SCCP_RI_SSN_PC &&
	    translated.ri != OSMO_SCCP_RI_SSN_IP) {
		/* TODO: GT Routing */
		LOGPSCI(inst, LOGL_NOTICE, "GT Routing not implemented yet\n");
#if 1
		/* Prevent endless recursion, see OS#2666. */
		sccp_rout_fail_enqueue(inst, xua, SCCP_RETURN_CAUSE_SUBSYSTEM_FAILURE, false);
		return 0;
#else
		/* Node 7 (Sheet 5) */
		return scrc_node_7(inst, xua, called);
#endif
	}

	/* Check DPC resultant from GT translation */
	if (osmo_ss7_pc_is_local(inst->ss7, translated.pc)) {
		if (sua_is_connectionless(xua)) {
			/* CL_MSG -> SCLC */
			sccp_sclc_rx_from_scrc(inst, xua);
		} else {
			/* Node 1 (Sheet 3) */
			/* CO_MSG -> SCOC */
			sccp_scoc_rx_from_scrc(inst, xua);
		}
		return 0;
	} else {
		/* Availability already checked */
		/* Node 7 (Sheet 5) */
		return scrc_node_7(inst, xua, called);
	}
}

/* Node 6 (Sheet 3) */
static int scrc_node_6(struct osmo_sccp_instance *inst,
		       struct xua_msg *xua,
		       const struct osmo_sccp_addr *called)
{
	struct osmo_sccp_user *scu;
	/* it is not really clear that called->pc will be set to
	 * anything here, in the case of a SSN-only CalledAddr */
	scu = sccp_user_find(inst, called->ssn, called->pc);

	/* Is subsystem equipped? */
	if (!scu) {
		/* Error: unequipped user */
		LOGPSCI(inst, LOGL_NOTICE, "Unable to find user for SSN=%u PC=%s\n",
			called->ssn, osmo_ss7_pointcode_print(inst->ss7, called->pc));
		return scrc_node_4(inst, xua,
				   SCCP_RETURN_CAUSE_UNEQUIPPED_USER);
	}
	/* Is subsystem available? */
	if (0 /* !subsys_available(scu) */) {
		/* Error: subsystem failure */
		/* TODO: SCRC -> SSPC */
		if (sua_is_connectionless(xua)) {
			/* Routing Failure SCRC -> SCLC */
			sccp_rout_fail_enqueue(inst, xua, SCCP_RETURN_CAUSE_SUBSYSTEM_FAILURE, false);
		} else {
			/* Routing Failure SCRC -> SCOC */
			sccp_rout_fail_enqueue(inst, xua, SCCP_RETURN_CAUSE_SUBSYSTEM_FAILURE, true);
		}
		return 0;
	}
	if (sua_is_connectionless(xua)) {
		/* CL_MSG -> SCLC */
		sccp_sclc_rx_from_scrc(inst, xua);
	} else {
		/* Node 1 (Sheet 3) */
		/* CO_MSG -> SCOC */
		sccp_scoc_rx_from_scrc(inst, xua);
	}
	return 0;
}

/* "Figure C.1/Q.714 - SCCP routing control procedures (SCRC) (sheet 1 of 12)",
 * Group of blocks on the bottom-right starting from "Call-ed address includes DPC"
 */
static int scrc_local_out_common(struct osmo_sccp_instance *inst,
				 struct xua_msg *xua,
				 const struct osmo_sccp_addr *called)
{
	struct osmo_ss7_instance *s7i = inst->ss7;

	/* Called address includes DPC? */
	if (called->presence & OSMO_SCCP_ADDR_T_PC) {
		if (!osmo_ss7_pc_is_local(s7i, called->pc)) {
			/* Node 7 of sheet 5 */
			/* Coupling required: no */
			return scrc_node_12(inst, xua, called);
		}
		/* Called address includes SSN? */
		if (called->presence & OSMO_SCCP_ADDR_T_SSN) {
			if (/* TODO: check if we are doing global translation && */
			    (called->presence & OSMO_SCCP_ADDR_T_GT))
				return scrc_translate_node_9(inst, xua, called);
			else
				return scrc_node_6(inst, xua, called);
		}
	}
	/* No SSN in CalledAddr or no DPC included */
	if (!(called->presence & OSMO_SCCP_ADDR_T_GT)) {
		/* Error reason: Unqualified */
		/* TODO: Routing Failure SCRC -> OMAP */
		/* Node 4 (Sheet 3) */
		return scrc_node_4(inst, xua,
				   SCCP_RETURN_CAUSE_UNQUALIFIED);
	} else
		return scrc_translate_node_9(inst, xua, called);
}

/***********************************************************************
 * Entrance points from MTP, SCLC, SCOC, ...
 ***********************************************************************/

/* Figure C.1/Q.714 - SCCP Routing control procedures (SCRC) */

/* Connection oriented message SCOC -> SCRC */
int sccp_scrc_rx_scoc_conn_msg(struct osmo_sccp_instance *inst,
				struct xua_msg *xua)
{
	struct osmo_sccp_addr called;
	int rc;

	LOGPSCI(inst, LOGL_DEBUG, "%s: %s\n", __func__, xua_msg_dump(xua, &xua_dialect_sua));

	/* Is this a CR message ? */
	if (xua->hdr.msg_type != SUA_CO_CORE)
		return scrc_node_2(inst, xua);

#if 0
	if (/* TODO: Coupling performed (not supported) */)
		return scrc_node_2(inst, xua);
#endif

	rc = sua_addr_parse(&called, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid DEST_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return -EINVAL;
	}

	return scrc_local_out_common(inst, xua, &called);
}

/* Connectionless Message SCLC -> SCRC */
int sccp_scrc_rx_sclc_msg(struct osmo_sccp_instance *inst,
			  struct xua_msg *xua)
{
	struct osmo_sccp_addr called;
	int rc;

	LOGPSCI(inst, LOGL_DEBUG, "%s: %s\n", __func__, xua_msg_dump(xua, &xua_dialect_sua));

	rc = sua_addr_parse(&called, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid DEST_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return -EINVAL;
	}

	/* Message Type */
	if (xua->hdr.msg_type == SUA_CL_CLDR) {
		/* UDTS, XUDTS or LUDTS */
		if (called.ri != OSMO_SCCP_RI_GT)
			return scrc_node_7(inst, xua, &called);
		/* Fall-through */
	} else {
		if (0 /* TODO: translation already performed */) {
			/* Node 12 (Sheet 5) */
			return scrc_node_12(inst, xua, &called);
		}
	}

	return scrc_local_out_common(inst, xua, &called);
}

/* ensure the CallingParty address doesn't just contain SSN, but at least SSN+PC */
static int ensure_opc_in_calling_ssn(struct osmo_sccp_instance *inst,
				      struct xua_msg *xua)
{
	struct osmo_sccp_addr calling;
	int rc;

	rc = sua_addr_parse(&calling, xua, SUA_IEI_SRC_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid SRC_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return -EINVAL;
	}

	/* if we route on SSN and only have a SSN in the address... */
	if (calling.ri == OSMO_SCCP_RI_SSN_PC &&
	    calling.presence == OSMO_SCCP_ADDR_T_SSN) {
		/* add the M3UA OPC to the address to ensure that the recipient
		 * can actually respond back to the source */
		calling.presence |= OSMO_SCCP_ADDR_T_PC;
		calling.pc = xua->mtp.opc;
		xua_msg_free_tag(xua, SUA_IEI_SRC_ADDR);
		xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &calling);
	}
	return 0;
}

/* Figure C.1/Q.714 Sheet 1 of 12, after we converted the
 * MTP-TRANSFER.ind to SUA. */
int scrc_rx_mtp_xfer_ind_xua(struct osmo_sccp_instance *inst,
			     struct xua_msg *xua)
{
	struct osmo_sccp_addr called;
	uint32_t proto_class;
	struct xua_msg_part *hop_ctr_part;
	int rc;

	LOGPSCI(inst, LOGL_DEBUG, "%s: %s\n", __func__, xua_msg_dump(xua, &xua_dialect_sua));
	/* TODO: SCCP or nodal congestion? */

	/* CR or CL message? */
	if (!sua_is_connectionless(xua) && !sua_is_cr(xua)) {
		/* Node 1 (Sheet 3) */
		/* deliver to SCOC */
		sccp_scoc_rx_from_scrc(inst, xua);
		return 0;
	}
	/* We only treat connectionless and CR below */

	/* ensure we have at least OPC+SSN and not just SSN in CallingParty (OS#5146) */
	rc = ensure_opc_in_calling_ssn(inst, xua);
	if (rc < 0)
		return -EINVAL;

	rc = sua_addr_parse(&called, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid DEST_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return -EINVAL;
	}

	/* Route on GT? */
	if (called.ri != OSMO_SCCP_RI_GT) {
		/* Node 6 (Sheet 3) */
		return scrc_node_6(inst, xua, &called);
	}
	/* Message with hop-counter? */
	hop_ctr_part = xua_msg_find_tag(xua, SUA_IEI_S7_HOP_CTR);
	if (hop_ctr_part) {
		uint32_t hop_counter = xua_msg_part_get_u32(hop_ctr_part);
		if (hop_counter <= 1) {
			/* Error: hop-counter violation */
			/* node 4 */
			return scrc_node_4(inst, xua, SCCP_RETURN_CAUSE_HOP_COUNTER_VIOLATION);
		}
		/* Decrement hop-counter */
		hop_counter--;
		*(uint32_t *)hop_ctr_part->dat = htonl(hop_counter);
	}

	/* node 3 (Sheet 2) */
	/* Protocol class 0? */
	proto_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS);
	switch (proto_class) {
	case 0:
		/* TODO: Assign SLS */
		break;
	case 1:
		/* TODO: Map incoming SLS to outgoing SLS */
		break;
	default:
		break;
	}
	return scrc_translate_node_9(inst, xua, &called);
}
