/* realmd -- Realm configuration service
 *
 * Copyright 2012 Red Hat Inc
 *
 * This program 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 of the licence or (at
 * your option) any later version.
 *
 * See the included COPYING file for more information.
 *
 * Author: Stef Walter <stefw@gnome.org>
 */

#include "config.h"

#include "realm-command.h"
#include "realm-dbus-constants.h"
#include "realm-diagnostics.h"
#include "realm-disco.h"
#include "realm-disco-dns.h"
#include "realm-disco-domain.h"
#include "realm-disco-mscldap.h"
#include "realm-disco-rootdse.h"
#include "realm-errors.h"
#include "realm-invocation.h"
#include "realm-network.h"

#include <glib/gi18n.h>

typedef struct _Callback {
	GAsyncReadyCallback function;
	gpointer user_data;
	struct _Callback *next;
} Callback;

typedef struct {
	GObject parent;
	gchar *input;
	GCancellable *cancellable;
	GDBusMethodInvocation *invocation;
	GSocketAddressEnumerator *enumerator;
	gint outstanding;
	gboolean completed;
	RealmDisco *disco;
	Callback *callback;
} RealmDiscoDomain;

typedef struct {
	GObjectClass parent;
} RealmDiscoDomainClass;

#define REALM_TYPE_DISCO_DOMAIN      (realm_disco_domain_get_type ())
#define REALM_DISCO_DOMAIN(inst)     (G_TYPE_CHECK_INSTANCE_CAST ((inst), REALM_TYPE_DISCO_DOMAIN, RealmDiscoDomain))
#define REALM_IS_DISCO_DOMAIN(inst)  (G_TYPE_CHECK_INSTANCE_TYPE ((inst), REALM_TYPE_DISCO_DOMAIN))

static GHashTable *discover_cache = NULL;

static void  step_discover  (RealmDiscoDomain *self,
                             RealmDisco *disco);

GType realm_disco_domain_get_type (void) G_GNUC_CONST;

void  realm_disco_domain_async_result_init (GAsyncResultIface *iface);

G_DEFINE_TYPE_WITH_CODE (RealmDiscoDomain, realm_disco_domain, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_RESULT, realm_disco_domain_async_result_init);
);

static void
realm_disco_domain_init (RealmDiscoDomain *self)
{
	self->cancellable = g_cancellable_new ();
}

static void
realm_disco_domain_finalize (GObject *obj)
{
	RealmDiscoDomain *self = REALM_DISCO_DOMAIN (obj);

	g_free (self->input);
	g_object_unref (self->cancellable);
	g_object_unref (self->invocation);
	g_clear_object (&self->enumerator);
	realm_disco_unref (self->disco);

	g_assert (self->callback == NULL);
	G_OBJECT_CLASS (realm_disco_domain_parent_class)->finalize (obj);
}

static void
realm_disco_domain_class_init (RealmDiscoDomainClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->finalize = realm_disco_domain_finalize;
}

static GObject *
realm_disco_domain_get_source_object (GAsyncResult *result)
{
	return g_object_ref (result);
}

static gpointer
realm_disco_domain_get_user_data (GAsyncResult *result)
{
	/* What is this useful for? */
	g_return_val_if_reached (NULL);
}

void
realm_disco_domain_async_result_init (GAsyncResultIface *iface)
{
	iface->get_source_object = realm_disco_domain_get_source_object;
	iface->get_user_data = realm_disco_domain_get_user_data;
}

static void
complete_discover (RealmDiscoDomain *self)
{
	Callback *call, *next;

	g_assert (!self->completed);
	self->completed = TRUE;

	/* No longer in the concurrency cache */
	g_hash_table_remove (discover_cache, self->input);
	if (g_hash_table_size (discover_cache) == 0)
		g_hash_table_destroy (discover_cache);
	discover_cache = NULL;

	/* Stop all other results */
	g_cancellable_cancel (self->cancellable);

	call = self->callback;
	self->callback = NULL;

	if (self->disco)
		realm_diagnostics_info (self->invocation, "Successfully discovered: %s", self->disco->domain_name);

	while (call != NULL) {
		next = call->next;
		if (call->function)
			(call->function) (NULL, G_ASYNC_RESULT (self), call->user_data);
		g_free (call);
		call = next;
	}
}

static void
on_discover_rootdse (GObject *source,
                     GAsyncResult *result,
                     gpointer user_data)
{
	RealmDiscoDomain *self = REALM_DISCO_DOMAIN (user_data);
	GError *error = NULL;
	RealmDisco *disco;

	self->outstanding--;
	disco = realm_disco_rootdse_finish (result, &error);

	if (error && !self->completed)
		realm_diagnostics_error (self->invocation, error, NULL);
	g_clear_error (&error);
	step_discover (self, disco);

	g_object_unref (self);
}

static void
on_discover_next_address (GObject *source,
                          GAsyncResult *result,
                          gpointer user_data)
{
	RealmDiscoDomain *self = REALM_DISCO_DOMAIN (user_data);
	GSocketAddressEnumerator *enumerator = G_SOCKET_ADDRESS_ENUMERATOR (source);
	GError *error = NULL;
	GSocketAddress *address;
	GInetSocketAddress *inet;
	const gchar *explicit_host;
	RealmDiscoDnsHint hint;
	gchar *string;

	if (self->completed) {
		g_object_unref (self);
		return;
	}

	address = g_socket_address_enumerator_next_finish (enumerator, result, &error);
	if (error != NULL || address == NULL) {
		if (error && !self->completed && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			realm_diagnostics_error (self->invocation, error, "Couldn't lookup realm DNS records");
		g_clear_error (&error);
		g_clear_object (&self->enumerator);

	} else {
		inet = G_INET_SOCKET_ADDRESS (address);
		string = g_inet_address_to_string (g_inet_socket_address_get_address (inet));

		hint = realm_disco_dns_get_hint (enumerator);
		if (hint & REALM_DISCO_IS_SERVER)
			explicit_host = realm_disco_dns_get_name (enumerator);
		else
			explicit_host = NULL;

		realm_diagnostics_info (self->invocation, "Performing LDAP DSE lookup on: %s", string);
		realm_disco_rootdse_async (address, explicit_host,
		                           self->invocation, self->cancellable,
		                           on_discover_rootdse, g_object_ref (self));
		self->outstanding++;
		g_free (string);
	}

	step_discover (self, NULL);
	g_clear_object (&address);
	g_object_unref (self);
}

static void
step_discover (RealmDiscoDomain *self,
               RealmDisco *disco)
{
	/* Already done, just skip these results */
	if (self->completed) {
		realm_disco_unref (disco);

	/* Either have a result, or finished searching: done */
	} else if (disco || (self->enumerator == NULL && self->outstanding == 0)) {
		self->disco = disco;
		complete_discover (self);

	/* Otherwise try up to three servers at once */
	} else if (self->enumerator && self->outstanding < 3) {
		g_socket_address_enumerator_next_async (self->enumerator,
		                                        self->cancellable,
		                                        on_discover_next_address,
		                                        g_object_ref (self));
	}
}

static void
on_cancel_propagate (GCancellable *source,
                     gpointer dest)
{
	g_cancellable_cancel (dest);
}

void
realm_disco_domain_async (const gchar *string,
                          GDBusMethodInvocation *invocation,
                          GAsyncReadyCallback callback,
                          gpointer user_data)
{
	RealmDiscoDomain *self;
	GCancellable *cancellable;
	Callback *call;

	g_return_if_fail (string != NULL);
	g_return_if_fail (invocation == NULL || G_IS_DBUS_METHOD_INVOCATION (invocation));

	if (!discover_cache)
		discover_cache = g_hash_table_new (g_str_hash, g_str_equal);

	self = g_hash_table_lookup (discover_cache, string);

	if (self == NULL) {
		self = g_object_new (REALM_TYPE_DISCO_DOMAIN, NULL);
		self->input = g_strdup (string);
		self->invocation = g_object_ref (invocation);
		self->enumerator = realm_disco_dns_enumerate_servers (string, invocation);

		g_hash_table_insert (discover_cache, self->input, self);
		g_assert (!self->completed);

		cancellable = realm_invocation_get_cancellable (invocation);
		if (cancellable) {
			g_cancellable_connect (cancellable, (GCallback)on_cancel_propagate,
			                       g_object_ref (self->cancellable), g_object_unref);
		}

		step_discover (self, NULL);

	} else {
		g_assert (!self->completed);
		g_object_ref (self);
	}

	call = g_new0 (Callback, 1);
	call->function = callback;
	call->user_data = user_data;
	call->next = self->callback;
	self->callback = call;

	g_object_unref (self);
}

RealmDisco *
realm_disco_domain_finish (GAsyncResult *result,
                           GError **error)
{
	RealmDiscoDomain *self;

	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	self = REALM_DISCO_DOMAIN (result);

	/* Didn't find a valid domain */
	if (!self->disco)
		return NULL;

	return realm_disco_ref (self->disco);
}
