/*
 * 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 "haslgs2header.h"

#include "haslenums.h"
#include "haslsaslname.h"
#include "haslstring.h"

enum {
	PROP_0,
	PROP_STANDARD_MECHANISM,
	PROP_CHANNEL_BINDING,
	PROP_CHANNEL_BINDING_NAME,
	PROP_AUTHZID,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _HaslGs2Header {
	GObject parent;

	gboolean standard_mechanism;

	HaslGs2HeaderChannelBinding channel_binding;
	char *channel_binding_name;

	char *authzid;
};

G_DEFINE_QUARK(hasl-gs2-header-error, hasl_gs2_header_error)

G_DEFINE_TYPE(HaslGs2Header, hasl_gs2_header, G_TYPE_OBJECT)

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
static void
hasl_gs2_header_get_property(GObject *obj, guint param_id, GValue *value,
                             GParamSpec *pspec)
{
	HaslGs2Header *header = HASL_GS2_HEADER(obj);

	switch(param_id) {
	case PROP_STANDARD_MECHANISM:
		g_value_set_boolean(value,
		                    hasl_gs2_header_get_standard_mechanism(header));
		break;
	case PROP_CHANNEL_BINDING:
		g_value_set_enum(value, hasl_gs2_header_get_channel_binding(header));
		break;
	case PROP_CHANNEL_BINDING_NAME:
		g_value_set_string(value,
		                   hasl_gs2_header_get_channel_binding_name(header));
		break;
	case PROP_AUTHZID:
		g_value_set_string(value, hasl_gs2_header_get_authzid(header));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
hasl_gs2_header_set_property(GObject *obj, guint param_id, const GValue *value,
                             GParamSpec *pspec)
{
	HaslGs2Header *header = HASL_GS2_HEADER(obj);

	switch(param_id) {
	case PROP_STANDARD_MECHANISM:
		hasl_gs2_header_set_standard_mechanism(header,
		                                       g_value_get_boolean(value));
		break;
	case PROP_CHANNEL_BINDING:
		hasl_gs2_header_set_channel_binding(header, g_value_get_enum(value));
		break;
	case PROP_CHANNEL_BINDING_NAME:
		hasl_gs2_header_set_channel_binding_name(header,
		                                         g_value_get_string(value));
		break;
	case PROP_AUTHZID:
		hasl_gs2_header_set_authzid(header, g_value_get_string(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
hasl_gs2_header_init(HaslGs2Header *header) {
	header->standard_mechanism = TRUE;
	header->channel_binding = HaslGs2HeaderChannelBindingClientNotSupported;
}

static void
hasl_gs2_header_finalize(GObject *obj) {
	HaslGs2Header *header = HASL_GS2_HEADER(obj);

	g_clear_pointer(&header->channel_binding_name, g_free);
	g_clear_pointer(&header->authzid, g_free);

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

static void
hasl_gs2_header_class_init(HaslGs2HeaderClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = hasl_gs2_header_finalize;
	obj_class->get_property = hasl_gs2_header_get_property;
	obj_class->set_property = hasl_gs2_header_set_property;

	/**
	 * HaslGs2Header:standard-mechanism:
	 *
	 * Whether or not the mechanism is a standard GSS-API mechanism.
	 *
	 * Since: 0.3
	 */
	properties[PROP_STANDARD_MECHANISM] = g_param_spec_boolean(
		"standard-mechanism", NULL, NULL,
		TRUE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslGs2Header:channel-binding:
	 *
	 * The channel binding flag for this header.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CHANNEL_BINDING] = g_param_spec_enum(
		"channel-binding", NULL, NULL,
		HASL_TYPE_GS2_HEADER_CHANNEL_BINDING,
		HaslGs2HeaderChannelBindingClientNotSupported,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * HaslGs2Header:channel-binding-name:
	 *
	 * The name of the channel binding data to use.
	 *
	 * If [property@Gs2Header:channel-binding] is set to ClientSupported, this
	 * must not be %NULL.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CHANNEL_BINDING_NAME] = g_param_spec_string(
		"channel-binding-name", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);


	/**
	 * HaslGs2Header:authzid:
	 *
	 * An optional authzid to use. This is the username to authenticate as when
	 * using admin/moderator credentials.
	 *
	 * Since: 0.3
	 */
	properties[PROP_AUTHZID] = g_param_spec_string(
		"authzid", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
HaslGs2Header *
hasl_gs2_header_new(void) {
	return g_object_new(HASL_TYPE_GS2_HEADER, NULL);
}

HaslGs2Header *
hasl_gs2_header_parse(const char *str, GError **error) {
	g_return_val_if_fail(str != NULL, NULL);

	return hasl_gs2_header_parse_full(str, NULL, error);
}

HaslGs2Header *
hasl_gs2_header_parse_full(const char *str, int *length, GError **error) {
	HaslGs2Header *header = NULL;
	GError *local_error = NULL;
	GMatchInfo *info = NULL;
	GRegex *regex = NULL;
	const char *pattern = "^(?:(?P<nonstd>F),)?"
	                      "(?P<cb>[npy])(?:=(?P<cbname>[^,]*))?,"
	                      "(?:a=(?P<authzid>[^,]+))?,";
	gboolean matched = FALSE;

	g_return_val_if_fail(str != NULL, NULL);

	regex = g_regex_new(pattern, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT,
	                    &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);
		return NULL;
	}

	matched = g_regex_match(regex, str, G_REGEX_MATCH_DEFAULT, &info);
	if(matched) {
		char *value = NULL;
		gboolean cbname_required = FALSE;

		header = hasl_gs2_header_new();

		/* Handle the standard mechanism field. */
		value = g_match_info_fetch_named(info, "nonstd");
		if(value[0] == 'F') {
			hasl_gs2_header_set_standard_mechanism(header, FALSE);
		}
		g_clear_pointer(&value, g_free);

		/* Handle the channel binding field. */
		value = g_match_info_fetch_named(info, "cb");
		switch(value[0]) {
		case 'n':
			hasl_gs2_header_set_channel_binding(header,
			                                    HaslGs2HeaderChannelBindingClientNotSupported);
			break;
		case 'p':
			hasl_gs2_header_set_channel_binding(header,
			                                    HaslGs2HeaderChannelBindingClientSupported);
			cbname_required = TRUE;
			break;
		case 'y':
			hasl_gs2_header_set_channel_binding(header,
			                                    HaslGs2HeaderChannelBindingClientSupportedServerUnknown);
			break;

		}
		g_clear_pointer(&value, g_free);

		/* Handle the cbname field. */
		value = g_match_info_fetch_named(info, "cbname");
		if(cbname_required) {
			if(hasl_str_is_empty(value)) {
				g_clear_pointer(&value, g_free);
				g_clear_object(&header);
				g_clear_pointer(&regex, g_regex_unref);
				g_clear_pointer(&info, g_match_info_unref);

				g_set_error_literal(error,
				                    HASL_GS2_HEADER_ERROR,
				                    HASL_GS2_HEADER_ERROR_CBNAME_NOT_SPECIFIED,
				                    "cbname was required but not specified.");

				return NULL;
			}

			hasl_gs2_header_set_channel_binding_name(header, value);
		} else if(!hasl_str_is_empty(value)) {
			g_clear_pointer(&value, g_free);
			g_clear_object(&header);
			g_clear_pointer(&regex, g_regex_unref);
			g_clear_pointer(&info, g_match_info_unref);

			g_set_error_literal(error,
			                    HASL_GS2_HEADER_ERROR,
			                    HASL_GS2_HEADER_ERROR_CBNAME_UNEXPECTED,
			                    "cbname was specified when it wasn't expected");

			return NULL;
		}
		g_clear_pointer(&value, g_free);

		/* Handle the authzid field. */
		value = g_match_info_fetch_named(info, "authzid");
		if(value != NULL && value[0] != '\0') {
			hasl_gs2_header_set_authzid(header, value);
		}
		g_clear_pointer(&value, g_free);
	}

	if(length != NULL) {
		int start = 0;
		int end = 0;

		g_match_info_fetch_pos(info, 0, &start, &end);

		*length = (end - start);
	}

	g_clear_pointer(&regex, g_regex_unref);
	g_clear_pointer(&info, g_match_info_unref);

	return header;
}

char *
hasl_gs2_header_to_string(HaslGs2Header *header, GError **error) {
	GString *str = NULL;

	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), NULL);

	str = g_string_new("");

	if(!header->standard_mechanism) {
		g_string_append(str, "F,");
	}

	/* Add our channel binding information. */
	if(header->channel_binding == HaslGs2HeaderChannelBindingClientSupported) {
		gboolean invalid = FALSE;

		if(hasl_str_is_empty(header->channel_binding_name)) {
			g_set_error_literal(error,
			                    HASL_GS2_HEADER_ERROR,
			                    HASL_GS2_HEADER_ERROR_CBNAME_NOT_SPECIFIED,
			                    "channel binding was set to supported but no "
			                    "name was given.");

			g_string_free(str, TRUE);

			return NULL;
		}

		/* Look to see if we have any invalid characters in the name. */
		invalid = g_regex_match_simple("[^A-Za-z0-9.-]",
		                               header->channel_binding_name,
		                               G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT);
		if(invalid) {
			g_set_error(error,
			            HASL_GS2_HEADER_ERROR,
			            HASL_GS2_HEADER_ERROR_CBNAME_INVALID,
			            "channel binding name \"%s\" contains invalid "
			            "characters. Only alphanumeric . and - are supported",
			            header->channel_binding_name);

			g_string_free(str, TRUE);

			return NULL;
		}

		g_string_append_printf(str, "p=%s", header->channel_binding_name);
	} else if(header->channel_binding == HaslGs2HeaderChannelBindingClientNotSupported)
	{
		g_string_append(str, "n");
	} else if(header->channel_binding == HaslGs2HeaderChannelBindingClientSupportedServerUnknown)
	{
		g_string_append(str, "y");
	} else {
		g_set_error(error,
		            HASL_GS2_HEADER_ERROR,
		            HASL_GS2_HEADER_ERROR_CB_FLAG_UNKNOWN,
		            "unknown channel binding flag value %d",
		            header->channel_binding);

		g_string_free(str, TRUE);

		return NULL;
	}
	g_string_append(str, ",");

	/* Add the authzid if we have it. */
	if(header->authzid != NULL && header->authzid[0] != '\0') {
		char *encoded = NULL;

		encoded = hasl_saslname_encode(header->authzid);
		g_string_append_printf(str, "a=%s", encoded);
		g_free(encoded);
	}
	g_string_append(str, ",");

	return g_string_free_and_steal(str);
}

gboolean
hasl_gs2_header_get_standard_mechanism(HaslGs2Header *header) {
	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), TRUE);

	return header->standard_mechanism;
}

void
hasl_gs2_header_set_standard_mechanism(HaslGs2Header *header,
                                       gboolean standard)
{
	g_return_if_fail(HASL_IS_GS2_HEADER(header));

	if(header->standard_mechanism != standard) {
		header->standard_mechanism = standard;

		g_object_notify_by_pspec(G_OBJECT(header),
		                         properties[PROP_STANDARD_MECHANISM]);
	}
}

HaslGs2HeaderChannelBinding
hasl_gs2_header_get_channel_binding(HaslGs2Header *header) {
	g_return_val_if_fail(HASL_IS_GS2_HEADER(header),
	                     HaslGs2HeaderChannelBindingClientNotSupported);

	return header->channel_binding;
}

void
hasl_gs2_header_set_channel_binding(HaslGs2Header *header,
                                    HaslGs2HeaderChannelBinding channel_binding)
{
	g_return_if_fail(HASL_IS_GS2_HEADER(header));

	if(header->channel_binding != channel_binding) {
		header->channel_binding = channel_binding;

		g_object_notify_by_pspec(G_OBJECT(header),
		                         properties[PROP_CHANNEL_BINDING]);
	}
}

const char *
hasl_gs2_header_get_channel_binding_name(HaslGs2Header *header) {
	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), NULL);

	return header->channel_binding_name;
}

void
hasl_gs2_header_set_channel_binding_name(HaslGs2Header *header,
                                         const char *name)
{
	g_return_if_fail(HASL_IS_GS2_HEADER(header));

	if(g_set_str(&header->channel_binding_name, name)) {
		g_object_notify_by_pspec(G_OBJECT(header),
		                         properties[PROP_CHANNEL_BINDING_NAME]);
	}
}

const char *
hasl_gs2_header_get_authzid(HaslGs2Header *header) {
	g_return_val_if_fail(HASL_IS_GS2_HEADER(header), NULL);

	return header->authzid;
}

void
hasl_gs2_header_set_authzid(HaslGs2Header *header, const char *authzid) {
	g_return_if_fail(HASL_IS_GS2_HEADER(header));

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