/*
   Unix SMB/CIFS implementation.
   Main metadata server / Spotlight routines / Tracker backend

   Copyright (C) Ralph Boehme 2019

   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 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 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 "includes.h"
#include "lib/util/time_basic.h"
#include "mdssvc.h"
#include "mdssvc_tracker.h"
#include "lib/tevent_glib_glue.h"
#include "rpc_server/mdssvc/sparql_parser.tab.h"

#undef DBGC_CLASS
#define DBGC_CLASS DBGC_RPC_SRV

static struct mdssvc_tracker_ctx *mdssvc_tracker_ctx;

/************************************************
 * Tracker async callbacks
 ************************************************/

static void tracker_con_cb(GObject *object,
			   GAsyncResult *res,
			   gpointer user_data)
{
	struct mds_tracker_ctx *ctx = NULL;
	TrackerSparqlConnection *tracker_con = NULL;
	GError *error = NULL;

	tracker_con = tracker_sparql_connection_get_finish(res, &error);
	if (error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		/*
		 * If the async request was cancelled, user_data will already be
		 * talloc_free'd, so we must be carefully checking for
		 * G_IO_ERROR_CANCELLED before using user_data.
		 */
		DBG_ERR("Tracker connection cancelled\n");
		g_error_free(error);
		return;
	}
	/*
	 * Ok, we're not cancelled, we can now safely use user_data.
	 */
	ctx = talloc_get_type_abort(user_data, struct mds_tracker_ctx);
	ctx->async_pending = false;
	/*
	 * Check error again, above we only checked for G_IO_ERROR_CANCELLED.
	 */
	if (error) {
		DBG_ERR("Could not connect to Tracker: %s\n", error->message);
		g_error_free(error);
		return;
	}

	ctx->tracker_con = tracker_con;

	DBG_DEBUG("connected to Tracker\n");
}

static void tracker_cursor_cb(GObject *object,
			      GAsyncResult *res,
			      gpointer user_data);

static void tracker_query_cb(GObject *object,
			     GAsyncResult *res,
			     gpointer user_data)
{
	struct sl_tracker_query *tq = NULL;
	struct sl_query *slq = NULL;
	TrackerSparqlConnection *conn = NULL;
	TrackerSparqlCursor *cursor = NULL;
	GError *error = NULL;

	conn = TRACKER_SPARQL_CONNECTION(object);

	cursor = tracker_sparql_connection_query_finish(conn, res, &error);
	/*
	 * If the async request was cancelled, user_data will already be
	 * talloc_free'd, so we must be carefully checking for
	 * G_IO_ERROR_CANCELLED before using user_data.
	 */
	if (error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		DBG_ERR("Tracker query cancelled\n");
		if (cursor != NULL) {
			g_object_unref(cursor);
		}
		g_error_free(error);
		return;
	}
	/*
	 * Ok, we're not cancelled, we can now safely use user_data.
	 */
	tq = talloc_get_type_abort(user_data, struct sl_tracker_query);
	tq->async_pending = false;
	slq = tq->slq;
	/*
	 * Check error again, above we only checked for G_IO_ERROR_CANCELLED.
	 */
	if (error) {
		DBG_ERR("Tracker query error: %s\n", error->message);
		g_error_free(error);
		slq->state = SLQ_STATE_ERROR;
		return;
	}

	tq->cursor = cursor;
	slq->state = SLQ_STATE_RESULTS;

	tracker_sparql_cursor_next_async(tq->cursor,
					 tq->gcancellable,
					 tracker_cursor_cb,
					 tq);
	tq->async_pending = true;
}

static char *tracker_to_unix_path(TALLOC_CTX *mem_ctx, const char *uri)
{
	GFile *f = NULL;
	char *path = NULL;
	char *talloc_path = NULL;

	f = g_file_new_for_uri(uri);
	if (f == NULL) {
		return NULL;
	}

	path = g_file_get_path(f);
	g_object_unref(f);

	if (path == NULL) {
		return NULL;
	}

	talloc_path = talloc_strdup(mem_ctx, path);
	g_free(path);
	if (talloc_path == NULL) {
		return NULL;
	}

	return talloc_path;
}

static void tracker_cursor_cb(GObject *object,
			      GAsyncResult *res,
			      gpointer user_data)
{
	TrackerSparqlCursor *cursor = NULL;
	struct sl_tracker_query *tq = NULL;
	struct sl_query *slq = NULL;
	const gchar *uri = NULL;
	GError *error = NULL;
	char *path = NULL;
	gboolean more_results;
	bool ok;

	cursor = TRACKER_SPARQL_CURSOR(object);
	more_results = tracker_sparql_cursor_next_finish(cursor,
							 res,
							 &error);
	/*
	 * If the async request was cancelled, user_data will already be
	 * talloc_free'd, so we must be carefully checking for
	 * G_IO_ERROR_CANCELLED before using user_data.
	 */
	if (error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_error_free(error);
		g_object_unref(cursor);
		return;
	}
	/*
	 * Ok, we're not cancelled, we can now safely use user_data.
	 */
	tq = talloc_get_type_abort(user_data, struct sl_tracker_query);
	tq->async_pending = false;
	slq = tq->slq;
	/*
	 * Check error again, above we only checked for G_IO_ERROR_CANCELLED.
	 */
	if (error) {
		DBG_ERR("Tracker cursor: %s\n", error->message);
		g_error_free(error);
		slq->state = SLQ_STATE_ERROR;
		return;
	}

	SLQ_DEBUG(10, slq, "results");

	if (!more_results) {
		slq->state = SLQ_STATE_DONE;

		g_object_unref(tq->cursor);
		tq->cursor = NULL;

		g_object_unref(tq->gcancellable);
		tq->gcancellable = NULL;
		return;
	}

	uri = tracker_sparql_cursor_get_string(tq->cursor, 0, NULL);
	if (uri == NULL) {
		DBG_ERR("error fetching Tracker URI\n");
		slq->state = SLQ_STATE_ERROR;
		return;
	}

	path = tracker_to_unix_path(slq->query_results, uri);
	if (path == NULL) {
		DBG_ERR("error converting Tracker URI to path: %s\n", uri);
		slq->state = SLQ_STATE_ERROR;
		return;
	}

	ok = mds_add_result(slq, path);
	if (!ok) {
		DBG_ERR("error adding result for path: %s\n", uri);
		slq->state = SLQ_STATE_ERROR;
		return;
	}

	if (slq->query_results->num_results >= MAX_SL_RESULTS) {
		slq->state = SLQ_STATE_FULL;
		SLQ_DEBUG(10, slq, "full");
		return;
	}

	slq->state = SLQ_STATE_RESULTS;
	SLQ_DEBUG(10, slq, "cursor next");

	tracker_sparql_cursor_next_async(tq->cursor,
					 tq->gcancellable,
					 tracker_cursor_cb,
					 tq);
	tq->async_pending = true;
}

/*
 * This gets called once, even if the backend is not configured by the user
 */
static bool mdssvc_tracker_init(struct mdssvc_ctx *mdssvc_ctx)
{
	if (mdssvc_tracker_ctx != NULL) {
		return true;
	}

#if (GLIB_MAJOR_VERSION < 3) && (GLIB_MINOR_VERSION < 36)
	g_type_init();
#endif

	mdssvc_tracker_ctx = talloc_zero(mdssvc_ctx, struct mdssvc_tracker_ctx);
	if (mdssvc_tracker_ctx == NULL) {
		return false;
	}
	mdssvc_tracker_ctx->mdssvc_ctx = mdssvc_ctx;

	return true;
}

/*
 * This gets called per mdscmd_open / tcon. This runs initialisation code that
 * should only run if the tracker backend is actually used.
 */
static bool mdssvc_tracker_prepare(void)
{
	if (mdssvc_tracker_ctx->gmain_ctx != NULL) {
		/*
		 * Assuming everything is setup if gmain_ctx is.
		 */
		return true;
	}

	mdssvc_tracker_ctx->gmain_ctx = g_main_context_new();
	if (mdssvc_tracker_ctx->gmain_ctx == NULL) {
		DBG_ERR("error from g_main_context_new\n");
		return false;
	}

	mdssvc_tracker_ctx->glue = samba_tevent_glib_glue_create(
		mdssvc_tracker_ctx,
		mdssvc_tracker_ctx->mdssvc_ctx->ev_ctx,
		mdssvc_tracker_ctx->gmain_ctx);
	if (mdssvc_tracker_ctx->glue == NULL) {
		DBG_ERR("samba_tevent_glib_glue_create failed\n");
		g_object_unref(mdssvc_tracker_ctx->gmain_ctx);
		mdssvc_tracker_ctx->gmain_ctx = NULL;
		return false;
	}

	return true;
}

static bool mdssvc_tracker_shutdown(struct mdssvc_ctx *mdssvc_ctx)
{
	if (mdssvc_tracker_ctx == NULL) {
		return true;
	}

	if (mdssvc_tracker_ctx->gmain_ctx == NULL) {
		return true;
	}

	samba_tevent_glib_glue_quit(mdssvc_tracker_ctx->glue);
	TALLOC_FREE(mdssvc_tracker_ctx->glue);

	g_object_unref(mdssvc_tracker_ctx->gmain_ctx);
	mdssvc_tracker_ctx->gmain_ctx = NULL;
	return true;
}

static int mds_tracker_ctx_destructor(struct mds_tracker_ctx *ctx)
{
	/*
	 * Don't g_object_unref() the connection if there's an async request
	 * pending, it's used in the async callback and will be unreferenced
	 * there.
	 */
	if (ctx->async_pending) {
		g_cancellable_cancel(ctx->gcancellable);
		ctx->gcancellable = NULL;
		return 0;
	}

	if (ctx->tracker_con == NULL) {
		return 0;
	}
	g_object_unref(ctx->tracker_con);
	ctx->tracker_con = NULL;

	return 0;
}

static bool mds_tracker_connect(struct mds_ctx *mds_ctx)
{
	struct mds_tracker_ctx *ctx = NULL;
	bool ok;

	ok = mdssvc_tracker_prepare();
	if (!ok) {
		return false;
	}

	ctx = talloc_zero(mds_ctx, struct mds_tracker_ctx);
	if (ctx == NULL) {
		return false;
	}
	talloc_set_destructor(ctx, mds_tracker_ctx_destructor);

	ctx->mds_ctx = mds_ctx;

	ctx->gcancellable = g_cancellable_new();
	if (ctx->gcancellable == NULL) {
		DBG_ERR("error from g_cancellable_new\n");
		TALLOC_FREE(ctx);
		return false;
	}

	tracker_sparql_connection_get_async(ctx->gcancellable,
					    tracker_con_cb,
					    ctx);
	ctx->async_pending = true;

	mds_ctx->backend_private = ctx;

	return true;
}

static int tq_destructor(struct sl_tracker_query *tq)
{
	/*
	 * Don't g_object_unref() the cursor if there's an async request
	 * pending, it's used in the async callback and will be unreferenced
	 * there.
	 */
	if (tq->async_pending) {
		g_cancellable_cancel(tq->gcancellable);
		tq->gcancellable = NULL;
		return 0;
	}

	if (tq->cursor == NULL) {
		return 0;
	}
	g_object_unref(tq->cursor);
	tq->cursor = NULL;
	return 0;
}

static bool mds_tracker_search_start(struct sl_query *slq)
{
	struct mds_tracker_ctx *tmds_ctx = talloc_get_type_abort(
		slq->mds_ctx->backend_private, struct mds_tracker_ctx);
	struct sl_tracker_query *tq = NULL;
	char *escaped_scope = NULL;
	bool ok;

	if (tmds_ctx->tracker_con == NULL) {
		DBG_ERR("no connection to Tracker\n");
		return false;
	}

	tq = talloc_zero(slq, struct sl_tracker_query);
	if (tq == NULL) {
		return false;
	}
	tq->slq = slq;
	talloc_set_destructor(tq, tq_destructor);

	tq->gcancellable = g_cancellable_new();
	if (tq->gcancellable == NULL) {
		DBG_ERR("g_cancellable_new() failed\n");
		goto error;
	}

	escaped_scope = g_uri_escape_string(
				slq->path_scope,
				G_URI_RESERVED_CHARS_ALLOWED_IN_PATH,
				TRUE);
	if (escaped_scope == NULL) {
		goto error;
	}

	tq->path_scope = talloc_strdup(tq, escaped_scope);
	g_free(escaped_scope);
	escaped_scope = NULL;
	if (tq->path_scope == NULL) {
		goto error;
	}

	slq->backend_private = tq;

	ok = map_spotlight_to_sparql_query(slq);
	if (!ok) {
		/*
		 * Two cases:
		 *
		 * 1) the query string is "false", the parser returns
		 * an error for that. We're supposed to return -1
		 * here.
		 *
		 * 2) the parsing really failed, in that case we're
		 * probably supposed to return -1 too, this needs
		 * verification though
		 */
		goto error;
	}

	DBG_DEBUG("SPARQL query: \"%s\"\n", tq->sparql_query);

	tracker_sparql_connection_query_async(tmds_ctx->tracker_con,
					      tq->sparql_query,
					      tq->gcancellable,
					      tracker_query_cb,
					      tq);
	tq->async_pending = true;

	slq->state = SLQ_STATE_RUNNING;
	return true;
error:
	g_object_unref(tq->gcancellable);
	TALLOC_FREE(tq);
	slq->backend_private = NULL;
	return false;
}

static bool mds_tracker_search_cont(struct sl_query *slq)
{
	struct sl_tracker_query *tq = talloc_get_type_abort(
		slq->backend_private, struct sl_tracker_query);

	tracker_sparql_cursor_next_async(tq->cursor,
					 tq->gcancellable,
					 tracker_cursor_cb,
					 tq);
	tq->async_pending = true;

	return true;
}

struct mdssvc_backend mdsscv_backend_tracker = {
	.init = mdssvc_tracker_init,
	.shutdown = mdssvc_tracker_shutdown,
	.connect = mds_tracker_connect,
	.search_start = mds_tracker_search_start,
	.search_cont = mds_tracker_search_cont,
};
