/*
 * Copyright (C) 2023 Hasl Developers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include "haslcontext.h"

#include "haslmechanismanonymous.h"
#include "haslmechanismexternal.h"
#include "haslmechanismplain.h"
#include "haslstring.h"

enum {
	PROP_0,
	PROP_ALLOWED_MECHANISMS,
	PROP_SERVER_MECHANISMS,
	PROP_AUTHZID,
	PROP_USERNAME,
	PROP_PASSWORD,
	PROP_TLS,
	PROP_ALLOW_CLEAR_TEXT,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _HaslContext {
	GObject parent;

	char *allowed_mechanisms;
	char *server_mechanisms;

	char *authzid;
	char *username;
	char *password;

	gboolean tls;
	gboolean allow_clear_text;

	GHashTable *mechanisms;

	GStrv available_mechanisms;
	guint available_mechanisms_count;
	int current_mechanism_index;
	HaslMechanism *current_mechanism;
};

G_DEFINE_TYPE(HaslContext, hasl_context, G_TYPE_OBJECT)

G_DEFINE_QUARK(hasl-context-error, hasl_context_error)

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
static void
hasl_context_get_property(GObject *obj, guint param_id, GValue *value,
                          GParamSpec *pspec)
{
	HaslContext *ctx = HASL_CONTEXT(obj);

	switch(param_id) {
	case PROP_ALLOWED_MECHANISMS:
		g_value_set_string(value, hasl_context_get_allowed_mechanisms(ctx));
		break;
	case PROP_SERVER_MECHANISMS:
		g_value_set_string(value, hasl_context_get_server_mechanisms(ctx));
		break;
	case PROP_AUTHZID:
		g_value_set_string(value, hasl_context_get_authzid(ctx));
		break;
	case PROP_USERNAME:
		g_value_set_string(value, hasl_context_get_username(ctx));
		break;
	case PROP_PASSWORD:
		g_value_set_string(value, hasl_context_get_password(ctx));
		break;
	case PROP_TLS:
		g_value_set_boolean(value, hasl_context_get_tls(ctx));
		break;
	case PROP_ALLOW_CLEAR_TEXT:
		g_value_set_boolean(value, hasl_context_get_allow_clear_text(ctx));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
hasl_context_set_property(GObject *obj, guint param_id, const GValue *value,
                          GParamSpec *pspec)
{
	HaslContext *ctx = HASL_CONTEXT(obj);

	switch(param_id) {
	case PROP_ALLOWED_MECHANISMS:
		hasl_context_set_allowed_mechanisms(ctx, g_value_get_string(value));
		break;
	case PROP_SERVER_MECHANISMS:
		hasl_context_set_server_mechanisms(ctx, g_value_get_string(value));
		break;
	case PROP_AUTHZID:
		hasl_context_set_authzid(ctx, g_value_get_string(value));
		break;
	case PROP_USERNAME:
		hasl_context_set_username(ctx, g_value_get_string(value));
		break;
	case PROP_PASSWORD:
		hasl_context_set_password(ctx, g_value_get_string(value));
		break;
	case PROP_TLS:
		hasl_context_set_tls(ctx, g_value_get_boolean(value));
		break;
	case PROP_ALLOW_CLEAR_TEXT:
		hasl_context_set_allow_clear_text(ctx, g_value_get_boolean(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
hasl_context_init(HaslContext *ctx) {
	/* The mechanisms hash table is keyed on the name and has a value that is
	 * the GType of the HaslMechanism implementation.
	 */
	ctx->mechanisms = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
	                                        NULL);

	/* The current_mechanism_index needs to start at -1 so the first call to
	 * next will set it to 0.
	 */
	ctx->current_mechanism_index = -1;

	hasl_context_add_mechanism(ctx, "ANONYMOUS",
	                           HASL_TYPE_MECHANISM_ANONYMOUS);
	hasl_context_add_mechanism(ctx, "EXTERNAL", HASL_TYPE_MECHANISM_EXTERNAL);
	hasl_context_add_mechanism(ctx, "PLAIN", HASL_TYPE_MECHANISM_PLAIN);
}

static void
hasl_context_finalize(GObject *obj) {
	HaslContext *ctx = HASL_CONTEXT(obj);

	g_clear_pointer(&ctx->allowed_mechanisms, g_free);
	g_clear_pointer(&ctx->server_mechanisms, g_free);
	g_clear_pointer(&ctx->authzid, g_free);
	g_clear_pointer(&ctx->username, g_free);
	g_clear_pointer(&ctx->password, g_free);

	g_clear_pointer(&ctx->mechanisms, g_hash_table_unref);

	g_clear_pointer(&ctx->available_mechanisms, g_strfreev);
	g_clear_object(&ctx->current_mechanism);

	G_OBJECT_CLASS(hasl_context_parent_class)->finalize(obj);
}

static void
hasl_context_class_init(HaslContextClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = hasl_context_finalize;
	obj_class->get_property = hasl_context_get_property;
	obj_class->set_property = hasl_context_set_property;

	/**
	 * HaslContext:allowed-mechanisms:
	 *
	 * A list of mechanisms that are allowed to be used by this context. This
	 * list can be separated by white space, commas, or a combination of both.
	 *
	 * Since: 0.1
	 */
	properties[PROP_ALLOWED_MECHANISMS] = g_param_spec_string(
		"allowed-mechanisms", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslContext:server-mechanisms:
	 *
	 * A list of mechanisms that the server advertised. This list can be
	 * separated by white space, commas, or a combination of both.
	 *
	 * Since: 0.4
	 */
	properties[PROP_SERVER_MECHANISMS] = g_param_spec_string(
		"server-mechanisms", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslContext:authzid
	 *
	 * The identifier for the user who is authenticating.
	 *
	 * There are times when an administrator wants to authenticate as a user,
	 * so this property will be set to the username of the user to authenticate
	 * as, while the username and password fields will be proper values for the
	 * administrator.
	 *
	 * Since: 0.1
	 */
	properties[PROP_AUTHZID] = g_param_spec_string(
		"authzid", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslContext:username:
	 *
	 * The username of the user that is authenticating for mechanisms that need
	 * it.
	 *
	 * Since: 0.1
	 */
	properties[PROP_USERNAME] = g_param_spec_string(
		"username", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslContext:password:
	 *
	 * The password to use for mechanisms that need it.
	 *
	 * Since: 0.1
	 */
	properties[PROP_PASSWORD] = g_param_spec_string(
		"password", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslContext:tls:
	 *
	 * Tells the context whether or not the connection is being run over TLS or
	 * not.
	 *
	 * This is used in conjunction with
	 * [property@Hasl.Context:allow-clear-text] to determine if mechanisms that
	 * depend on the transport layer to secure credentials should be attempted.
	 *
	 * Since: 0.1
	 */
	properties[PROP_TLS] = g_param_spec_boolean(
		"tls", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslContext:allow-clear-text:
	 *
	 * Determines whether or not mechanisms that depend on transport layer
	 * encryption like TLS are allowed to be used on a non-TLS connection.
	 *
	 * If [property@Hasl.Context:tls] is %FALSE, then these mechanisms, like
	 * the `PLAIN` mechanism will only be attempted if this property is %TRUE.
	 *
	 * Since: 0.1
	 */
	properties[PROP_ALLOW_CLEAR_TEXT] = g_param_spec_boolean(
		"allow-clear-text", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
void
hasl_context_done(HaslContext *ctx) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	g_clear_object(&ctx->current_mechanism);
	ctx->current_mechanism_index = -1;

	g_clear_pointer(&ctx->available_mechanisms, g_strfreev);
	ctx->available_mechanisms_count = 0;
}

HaslContext *
hasl_context_new(void) {
	return g_object_new(HASL_TYPE_CONTEXT, NULL);
}

const char *
hasl_context_get_allowed_mechanisms(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	return ctx->allowed_mechanisms;
}

void
hasl_context_set_allowed_mechanisms(HaslContext *ctx, const char *mechanisms) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(g_set_str(&ctx->allowed_mechanisms, mechanisms)) {
		g_object_notify_by_pspec(G_OBJECT(ctx), properties[PROP_ALLOWED_MECHANISMS]);
	}
}

const char *
hasl_context_get_authzid(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	return ctx->authzid;
}

void
hasl_context_set_authzid(HaslContext *ctx, const char *authzid) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(g_set_str(&ctx->authzid, authzid)) {
		g_object_notify_by_pspec(G_OBJECT(ctx), properties[PROP_AUTHZID]);
	}
}

const char *
hasl_context_get_username(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	return ctx->username;
}

void
hasl_context_set_username(HaslContext *ctx, const char *username) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(g_set_str(&ctx->username, username)) {
		g_object_notify_by_pspec(G_OBJECT(ctx), properties[PROP_USERNAME]);
	}
}

const char *
hasl_context_get_password(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	return ctx->password;
}

void
hasl_context_set_password(HaslContext *ctx, const char *password) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(g_set_str(&ctx->password, password)) {
		g_object_notify_by_pspec(G_OBJECT(ctx), properties[PROP_PASSWORD]);
	}
}

gboolean
hasl_context_get_tls(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), FALSE);

	return ctx->tls;
}

void
hasl_context_set_tls(HaslContext *ctx, gboolean tls) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(tls != ctx->tls) {
		ctx->tls = tls;

		g_object_notify_by_pspec(G_OBJECT(ctx), properties[PROP_TLS]);
	}
}

gboolean
hasl_context_get_allow_clear_text(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), FALSE);

	return ctx->allow_clear_text;
}

void
hasl_context_set_allow_clear_text(HaslContext *ctx,
                                  gboolean allow_clear_text)
{
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(allow_clear_text != ctx->allow_clear_text) {
		ctx->allow_clear_text = allow_clear_text;

		g_object_notify_by_pspec(G_OBJECT(ctx),
		                         properties[PROP_ALLOW_CLEAR_TEXT]);
	}
}

gboolean
hasl_context_add_mechanism(HaslContext *ctx, const char *name, GType type) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), FALSE);
	g_return_val_if_fail(name != NULL && name[0] != '\0', FALSE);
	g_return_val_if_fail(g_type_is_a(type, HASL_TYPE_MECHANISM), FALSE);

	return g_hash_table_insert(ctx->mechanisms, g_strdup(name),
	                           GSIZE_TO_POINTER(type));
}

char *
hasl_context_get_supported_mechanisms(HaslContext *ctx) {
	GString *str = NULL;
	GList *names = NULL;
	gboolean first = TRUE;

	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	names = g_hash_table_get_keys(ctx->mechanisms);
	names = g_list_sort(names, (GCompareFunc)g_strcmp0);

	str = g_string_new("");

	while(names != NULL) {
		if(first) {
			first = FALSE;
		} else {
			g_string_append(str, " ");
		}

		g_string_append(str, (char *)names->data);

		names = g_list_delete_link(names, names);
	}

	return g_string_free_and_steal(str);
}

const char *
hasl_context_get_current_mechanism(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	/* hasl_context_next hasn't been called yet. */
	if(ctx->current_mechanism_index < 0) {
		return NULL;
	}

	/* hasl_context_set_allowed_mechanisms hasn't been called. */
	if(ctx->available_mechanisms == NULL) {
		return NULL;
	}

	if((guint)ctx->current_mechanism_index < ctx->available_mechanisms_count) {
		return ctx->available_mechanisms[ctx->current_mechanism_index];
	}

	return NULL;
}

const char *
hasl_context_get_server_mechanisms(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	return ctx->server_mechanisms;
}

void
hasl_context_set_server_mechanisms(HaslContext *ctx, const char *mechanisms) {
	g_return_if_fail(HASL_IS_CONTEXT(ctx));

	if(g_set_str(&ctx->server_mechanisms, mechanisms)) {
		g_object_notify_by_pspec(G_OBJECT(ctx),
		                         properties[PROP_SERVER_MECHANISMS]);
	}
}

const char *
hasl_context_next(HaslContext *ctx) {
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);

	/* If we don't have any available mechanisms check if the user set any,
	 * otherwise fall back to the server advertised ones. If we don't have
	 * either, we just return null.
	 */
	if(ctx->available_mechanisms == NULL) {
		const char *mechanisms = ctx->allowed_mechanisms;

		/* No client side mechanisms, try the server advertised ones. */
		if(hasl_str_is_empty(mechanisms)) {
			mechanisms = ctx->server_mechanisms;
		}

		/* No server advertised mechanisms, we fall back to supported below. */
		if(hasl_str_is_empty(mechanisms)) {
			mechanisms = NULL;
		}

		/* If we have mechanisms use them, otherwise fall back to the supported
		 * mechanisms.
		 */
		if(mechanisms != NULL) {
			/* We have mechanisms, so tokenize them and initialize everything. */
			ctx->available_mechanisms = g_strsplit_set(mechanisms, ", ", -1);
			ctx->available_mechanisms_count = g_strv_length(ctx->available_mechanisms);
		} else {
			char *supported = hasl_context_get_supported_mechanisms(ctx);

			ctx->available_mechanisms = g_strsplit_set(supported, ", ", -1);
			g_free(supported);

			ctx->available_mechanisms_count = g_strv_length(ctx->available_mechanisms);
		}

		ctx->current_mechanism_index = -1;
	}

	/* Now loop through the allowed mechanism until we find one that is
	 * possible or we've exhausted the list.
	 */
	while(TRUE) {
		GError *error = NULL;
		GType mechanism_type = G_TYPE_INVALID;
		const char *mechanism_name = NULL;
		int index = 0;
		gpointer value;

		/* Free the previous mechanism as we're done with it. */
		g_clear_object(&ctx->current_mechanism);

		/* Make sure we have another item before we increment our internal
		 * counter to make sure we can't overflow.
		 */
		index = ctx->current_mechanism_index + 1;
		ctx->current_mechanism_index = index;

		if((guint)index >= ctx->available_mechanisms_count) {
			return NULL;
		}

		mechanism_name = ctx->available_mechanisms[index];

		/* Find the type of the mechanism. */
		value = g_hash_table_lookup(ctx->mechanisms, mechanism_name);
		if(value == NULL) {
			continue;
		}

		/* Validate that the type is a HaslMechanism. */
		mechanism_type = GPOINTER_TO_SIZE(value);
		if(!g_type_is_a(mechanism_type, HASL_TYPE_MECHANISM)) {
			continue;
		}

		/* Create an instance of the mechanism and check if it's possible to
		 * use it.
		 */
		ctx->current_mechanism = g_object_new(mechanism_type, NULL);
		if(!hasl_mechanism_possible(ctx->current_mechanism, ctx, &error)) {
			g_message("skipping mechanism '%s': %s", mechanism_name,
			          (error != NULL) ? error->message : "unknown error");
			g_clear_error(&error);

			continue;
		}

		/* The mechanism should be usable, so return its name. */
		return mechanism_name;
	}

	return NULL;
}

HaslMechanismResult
hasl_context_step(HaslContext *ctx, const guint8 *server_in,
                  gsize server_in_length, guint8 **client_out,
                  gsize *client_out_length, GError **error)
{
	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), HASL_MECHANISM_RESULT_ERROR);
	g_return_val_if_fail(client_out != NULL, HASL_MECHANISM_RESULT_ERROR);

	if(!HASL_IS_MECHANISM(ctx->current_mechanism)) {
		g_set_error_literal(error,
		                    HASL_CONTEXT_ERROR,
		                    HASL_CONTEXT_ERROR_NO_MORE_MECHANISMS,
		                    _("current mechanism is NULL"));

		return HASL_MECHANISM_RESULT_ERROR;
	}

	return hasl_mechanism_step(ctx->current_mechanism, ctx,
	                           server_in, server_in_length,
	                           client_out, client_out_length,
	                           error);
}
