/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 *
 * All Rights Reserved
 *
 * 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 <errno.h>
#include <osmocom/core/logging.h>
#include <osmocom/mslookup/mslookup_client.h>
#include <osmocom/mslookup/mslookup_client_mdns.h>
#include <osmocom/gsupclient/gsup_client.h>
#include <osmocom/gsupclient/cni_peer_id.h>
#include <osmocom/hlr/logging.h>
#include <osmocom/hlr/hlr.h>
#include <osmocom/hlr/db.h>
#include <osmocom/hlr/gsup_router.h>
#include <osmocom/hlr/gsup_server.h>
#include <osmocom/hlr/dgsm.h>
#include <osmocom/hlr/proxy.h>
#include <osmocom/hlr/remote_hlr.h>
#include <osmocom/hlr/mslookup_server.h>
#include <osmocom/hlr/mslookup_server_mdns.h>
#include <osmocom/hlr/dgsm.h>

void *dgsm_ctx = NULL;

static void resolve_hlr_result_cb(struct osmo_mslookup_client *client,
				  uint32_t request_handle,
				  const struct osmo_mslookup_query *query,
				  const struct osmo_mslookup_result *result)
{
	struct proxy *proxy = g_hlr->gs->proxy;
	struct proxy_subscr proxy_subscr;
	const struct osmo_sockaddr_str *remote_hlr_addr;

	/* A remote HLR is answering back, indicating that it is the home HLR for a given IMSI.
	 * There should be a mostly empty proxy entry for that IMSI.
	 * Add the remote address data in the proxy. */
	if (query->id.type != OSMO_MSLOOKUP_ID_IMSI) {
		LOGP(DDGSM, LOGL_ERROR, "Expected IMSI ID type in mslookup query+result: %s\n",
		     osmo_mslookup_result_name_c(OTC_SELECT, query, result));
		return;
	}

	if (result->rc != OSMO_MSLOOKUP_RC_RESULT) {
		LOG_DGSM(query->id.imsi, LOGL_ERROR, "Failed to resolve remote HLR: %s\n",
			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
		proxy_subscr_del(proxy, query->id.imsi);
		return;
	}

	if (osmo_sockaddr_str_is_nonzero(&result->host_v4))
		remote_hlr_addr = &result->host_v4;
	else if (osmo_sockaddr_str_is_nonzero(&result->host_v6))
		remote_hlr_addr = &result->host_v6;
	else {
		LOG_DGSM(query->id.imsi, LOGL_ERROR, "Invalid address for remote HLR: %s\n",
			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
		proxy_subscr_del(proxy, query->id.imsi);
		return;
	}

	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, query->id.imsi)) {
		LOG_DGSM(query->id.imsi, LOGL_ERROR, "No proxy entry for mslookup result: %s\n",
			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
		return;
	}

	proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, remote_hlr_addr);
}

/* Return true when the message has been handled by D-GSM. */
bool dgsm_check_forward_gsup_msg(struct osmo_gsup_req *req)
{
	struct proxy_subscr proxy_subscr;
	struct proxy *proxy = g_hlr->gs->proxy;
	struct osmo_mslookup_query query;
	struct osmo_mslookup_query_handling handling;
	uint32_t request_handle;

	/* If the IMSI is known in the local HLR, then we won't proxy. */
	if (db_subscr_exists_by_imsi(g_hlr->dbc, req->gsup.imsi) == 0)
		return false;

	/* Are we already forwarding this IMSI to a remote HLR? */
	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi) == 0) {
		proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req);
		return true;
	}

	/* The IMSI is not known locally, so we want to proxy to a remote HLR, but no proxy entry exists yet. We need to
	 * look up the subscriber in remote HLRs via D-GSM mslookup, forward GSUP and reply once a result is back from
	 * there.  Defer message and kick off MS lookup. */

	/* Add a proxy entry without a remote address to indicate that we are busy querying for a remote HLR. */
	proxy_subscr = (struct proxy_subscr){};
	OSMO_STRLCPY_ARRAY(proxy_subscr.imsi, req->gsup.imsi);
	if (proxy_subscr_create_or_update(proxy, &proxy_subscr)) {
		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Failed to create proxy entry\n");
		return true;
	}

	/* Is a fixed gateway proxy configured? */
	if (osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy)) {
		proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, &g_hlr->mslookup.client.gsup_gateway_proxy);

		/* Proxy database modified, update info */
		if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi)) {
			osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Internal proxy error\n");
			return true;
		}

		proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req);
		return true;
	}

	/* Kick off an mslookup for the remote HLR?  This check could be up first on the top, but do it only now so that
	 * if the mslookup client disconnected, we still continue to service open proxy entries. */
	if (!osmo_mslookup_client_active(g_hlr->mslookup.client.client)) {
		LOG_GSUP_REQ(req, LOGL_DEBUG, "mslookup client not running, cannot query remote home HLR\n");
		return false;
	}

	/* First spool message, then kick off mslookup. If the proxy denies this message type, then don't do anything. */
	if (proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req)) {
		/* If the proxy denied forwarding, an error response was already generated. */
		return true;
	}

	query = (struct osmo_mslookup_query){
		.id = {
			.type = OSMO_MSLOOKUP_ID_IMSI,
		},
	};
	OSMO_STRLCPY_ARRAY(query.id.imsi, req->gsup.imsi);
	OSMO_STRLCPY_ARRAY(query.service, OSMO_MSLOOKUP_SERVICE_HLR_GSUP);
	handling = (struct osmo_mslookup_query_handling){
		.min_wait_milliseconds = g_hlr->mslookup.client.result_timeout_milliseconds,
		.result_cb = resolve_hlr_result_cb,
	};
	request_handle = osmo_mslookup_client_request(g_hlr->mslookup.client.client, &query, &handling);
	if (!request_handle) {
		LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Error dispatching mslookup query for home HLR: %s\n",
			 osmo_mslookup_result_name_c(OTC_SELECT, &query, NULL));
		proxy_subscr_del(proxy, req->gsup.imsi);
		/* mslookup seems to not be working. Try handling it locally. */
		return false;
	}

	return true;
}

void dgsm_init(void *ctx)
{
	dgsm_ctx = talloc_named_const(ctx, 0, "dgsm");
	INIT_LLIST_HEAD(&g_hlr->mslookup.server.local_site_services);

	g_hlr->mslookup.server.local_attach_max_age = 60 * 60;

	g_hlr->mslookup.client.result_timeout_milliseconds = OSMO_DGSM_DEFAULT_RESULT_TIMEOUT_MS;

	g_hlr->gsup_unit_name.unit_name = "HLR";
	g_hlr->gsup_unit_name.serno = "unnamed-HLR";
	g_hlr->gsup_unit_name.swversion = PACKAGE_NAME "-" PACKAGE_VERSION;

	osmo_sockaddr_str_from_str(&g_hlr->mslookup.server.mdns.bind_addr,
				   OSMO_MSLOOKUP_MDNS_IP4, OSMO_MSLOOKUP_MDNS_PORT);
	osmo_sockaddr_str_from_str(&g_hlr->mslookup.client.mdns.query_addr,
				   OSMO_MSLOOKUP_MDNS_IP4, OSMO_MSLOOKUP_MDNS_PORT);
}

void dgsm_start(void *ctx)
{
	g_hlr->mslookup.client.client = osmo_mslookup_client_new(dgsm_ctx);
	OSMO_ASSERT(g_hlr->mslookup.client.client);
	g_hlr->mslookup.allow_startup = true;
	mslookup_server_mdns_config_apply();
	dgsm_mdns_client_config_apply();
}

void dgsm_stop(void)
{
	g_hlr->mslookup.allow_startup = false;
	mslookup_server_mdns_config_apply();
	dgsm_mdns_client_config_apply();
}

void dgsm_mdns_client_config_apply(void)
{
	/* Check whether to start/stop/restart mDNS client */
	const struct osmo_sockaddr_str *current_bind_addr;
	const char *current_domain_suffix;
	current_bind_addr = osmo_mslookup_client_method_mdns_get_bind_addr(g_hlr->mslookup.client.mdns.running);
	current_domain_suffix = osmo_mslookup_client_method_mdns_get_domain_suffix(g_hlr->mslookup.client.mdns.running);

	bool should_run = g_hlr->mslookup.allow_startup
		&& g_hlr->mslookup.client.enable && g_hlr->mslookup.client.mdns.enable;

	bool should_stop = g_hlr->mslookup.client.mdns.running &&
		(!should_run
		 || osmo_sockaddr_str_cmp(&g_hlr->mslookup.client.mdns.query_addr,
					  current_bind_addr)
		 || strcmp(g_hlr->mslookup.client.mdns.domain_suffix,
			   current_domain_suffix));

	if (should_stop) {
		osmo_mslookup_client_method_del(g_hlr->mslookup.client.client, g_hlr->mslookup.client.mdns.running);
		g_hlr->mslookup.client.mdns.running = NULL;
		LOGP(DDGSM, LOGL_NOTICE, "Stopped mslookup mDNS client\n");
	}

	if (should_run && !g_hlr->mslookup.client.mdns.running) {
		g_hlr->mslookup.client.mdns.running =
			osmo_mslookup_client_add_mdns(g_hlr->mslookup.client.client,
						      g_hlr->mslookup.client.mdns.query_addr.ip,
						      g_hlr->mslookup.client.mdns.query_addr.port,
						      -1,
						      g_hlr->mslookup.client.mdns.domain_suffix);
		if (!g_hlr->mslookup.client.mdns.running)
			LOGP(DDGSM, LOGL_ERROR, "Failed to start mslookup mDNS client with target "
			     OSMO_SOCKADDR_STR_FMT "\n",
			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.mdns.query_addr));
		else
			LOGP(DDGSM, LOGL_NOTICE, "Started mslookup mDNS client, sending mDNS requests to multicast "
			     OSMO_SOCKADDR_STR_FMT "\n",
			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.mdns.query_addr));
	}

	if (g_hlr->mslookup.client.enable && osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy))
			LOGP(DDGSM, LOGL_NOTICE,
			     "mslookup client: all GSUP requests for unknown IMSIs will be forwarded to"
			     " gateway-proxy " OSMO_SOCKADDR_STR_FMT "\n",
			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.gsup_gateway_proxy));
}
