/*
 * This file is part of the webaccounts-browser-plugin.
 * Copyright (C) Canonical Ltd. 2012
 *
 * Author: Alberto Mardegan <alberto.mardegan@canonical.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * version 2.1 as published by the Free Software Foundation.
 *
 * 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 General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "login-handler.h"

#include <json-glib/json-glib.h>
#include <gio/gio.h>
#include <libaccounts-glib/ag-account.h>
#include <libaccounts-glib/ag-manager.h>
#include <libaccounts-glib/ag-provider.h>
#include <string.h>

#define WEBCREDENTIALS_CAPTURE_SERVICE "com.canonical.webcredentials.capture"
#define WEBCREDENTIALS_CAPTURE_OBJECT_PATH \
    "/com/canonical/webcredentials/capture"
#define WEBCREDENTIALS_CAPTURE_INTERFACE WEBCREDENTIALS_CAPTURE_SERVICE

#define WEBCREDENTIALS_DCONF_SCHEMA "com.canonical.webcredentials.capture"

/* Instantiating the manager is expensive; so, let's cache it */
static AgManager *manager = NULL;
static GDBusConnection *dbus_connection = NULL;

static AgProvider *
find_provider_for_domain (const gchar *domain)
{
    GList *providers, *list;
    AgProvider *result = NULL;

    g_return_val_if_fail (AG_IS_MANAGER (manager), NULL);

    providers = ag_manager_list_providers (manager);
    for (list = providers; list != NULL; list = list->next)
    {
        AgProvider *provider = list->data;
        const gchar *domains_regex;

        domains_regex = ag_provider_get_domains_regex (provider);
        if (domains_regex == NULL || domains_regex[0] == '\0')
        {
            continue;
        }

        if (g_regex_match_simple (domains_regex, domain, 0, 0))
        {
            /* Found a provider which supports this domain */
            result = ag_provider_ref (provider);
            break;
        }

    }

    ag_provider_list_free (providers);

    return result;
}

static void
blacklist_account (const gchar *domain, const gchar *username)
{
    GSettings *settings;
    gchar **accounts, **accounts_old;
    guint length, i;

    settings = g_settings_new (WEBCREDENTIALS_DCONF_SCHEMA);
    if (G_UNLIKELY (settings == NULL))
    {
        g_warning ("Failed to open DConf database");
        return;
    }

    accounts_old = g_settings_get_strv (settings, "dontask-accounts");
    length = g_strv_length (accounts_old);

    accounts = g_new (gchar *, length + 2);
    for (i = 0; i < length; i++)
    {
        accounts[i] = accounts_old[i];
    }
    accounts[i++] = g_strdup_printf ("%s:%s", domain, username);
    accounts[i] = NULL;

    g_settings_set_strv (settings, "dontask-accounts",
                         (const gchar * const*) accounts);

    g_object_unref (settings);
    g_strfreev (accounts);
    g_free (accounts_old);
}

static gboolean
account_blacklisted (const gchar *domain, const gchar *username)
{
    GSettings *settings;
    gchar **accounts;
    gchar *match;
    gint i;
    gboolean found = FALSE;

    settings = g_settings_new (WEBCREDENTIALS_DCONF_SCHEMA);
    if (G_UNLIKELY (settings == NULL))
    {
        g_warning ("Failed to open DConf database");
        return TRUE;
    }

    accounts = g_settings_get_strv (settings, "dontask-accounts");
    match = g_strdup_printf ("%s:%s", domain, username);

    for (i = 0; accounts[i] != NULL; i++)
    {
        if (strcmp (accounts[i], match) == 0)
        {
            found = TRUE;
            break;
        }
    }

    g_object_unref (settings);
    g_strfreev (accounts);
    g_free (match);
    return found;
}

static gboolean
account_exists (AgProvider *provider, const gchar *username)
{
    GList *accounts, *list;
    gboolean account_exists = FALSE;
    const gchar *provider_name;

    g_return_val_if_fail (AG_IS_MANAGER (manager), FALSE);

    /* An ag_manager_list_by_provider() method would be welcome here.
     * http://code.google.com/p/accounts-sso/issues/detail?id=59
     */
    provider_name = ag_provider_get_name (provider);
    accounts = ag_manager_list (manager);
    for (list = accounts; list != NULL; list = list->next)
    {
        AgAccountId account_id = (AgAccountId)GPOINTER_TO_INT(list->data);
        AgAccount *account;

        account = ag_manager_get_account (manager, account_id);
        if (G_UNLIKELY (account == NULL))
        {
            continue;
        }

        if (g_strcmp0 (ag_account_get_provider_name (account),
                       provider_name) != 0)
        {
            g_object_unref (account);
            continue;
        }

        /* Here we assume that the account username is used as display name; we
         * might need to revisit this later. */
        if (g_strcmp0 (ag_account_get_display_name (account), username) == 0)
        {
            g_object_unref (account);
            account_exists = TRUE;
            break;
        }
        g_object_unref (account);
    }

    ag_manager_list_free (accounts);

    return account_exists;
}

/*
 * Check whether the given account is relevant for the WebCredentials
 * implementationand, if it is, forward the information to the system settings
 * applet.
 */
void
webaccounts_store_login (const gchar *domain,
                         const gchar *username,
                         const gchar *password,
                         GVariant *cookies)
{
    AgProvider *provider;

    g_debug ("%s; domain %s, username %s, password %s", G_STRFUNC,
               domain, username, password);
    if (manager == NULL)
    {
        manager = ag_manager_new ();
    }

    if (domain == NULL || domain[0] == '\0' ||
        username == NULL || username[0] == '\0')
    {
        g_warning ("Insufficient informations (domain = '%s', username = '%s')",
                   domain, username);
        return;
    }

    provider = find_provider_for_domain (domain);
    if (provider == NULL)
    {
        g_debug ("Provider not found");
        /* The domain is not bound to any account provider; nothing to do here */
        goto nothing_to_do;
    }

    if (account_blacklisted (domain, username))
    {
        g_debug ("Account %s on %s is blacklisted", username, domain);
        goto nothing_to_do;
    }

    /* If an account for the same provider with the same username already
     * exists, stop processing here. */
    if (account_exists (provider, username))
    {
        g_debug ("Account for %s exists", username);
        goto nothing_to_do;
    }

    /* Cannot pass NULL strings to D-Bus */
    if (password == NULL)
    {
        password = "";
    }

    if (dbus_connection == NULL)
    {
        GError *error = NULL;

        dbus_connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
        if (error != NULL)
        {
            g_critical ("Failed to connect to session bus: %s", error->message);
            g_error_free (error);
            return;
        }
    }

    /* Asynchronous call, with no callback because we don't care about the
     * outcome of this call. */
    g_dbus_connection_call (dbus_connection,
                            WEBCREDENTIALS_CAPTURE_SERVICE,
                            WEBCREDENTIALS_CAPTURE_OBJECT_PATH,
                            WEBCREDENTIALS_CAPTURE_INTERFACE,
                            "LoginCaptured",
                            g_variant_new ("(sss@a{ss})",
                                           ag_provider_get_name (provider),
                                           username,
                                           password,
                                           cookies),
                            NULL,
                            G_DBUS_CALL_FLAGS_NONE,
                            -1,
                            NULL,
                            NULL,
                            NULL);

    /* Don't propose to create this account ever again */
    blacklist_account (domain, username);

nothing_to_do:
    if (provider != NULL)
    {
        ag_provider_unref (provider);
        provider = NULL;
    }
}

void
webaccounts_login_handler_set_json (const gchar *login_info_json)
{
    GVariant *login_info;
    GVariant *cookies_in;
    GVariant *cookies = NULL;
    GVariantBuilder builder;
    GError *error = NULL;
    gchar *username = NULL;
    gchar *password = NULL;
    gchar *domain = NULL;

    g_return_if_fail (login_info_json != NULL);

    login_info = json_gvariant_deserialize_data (login_info_json, -1, NULL,
                                                 &error);
    if (G_UNLIKELY (error != NULL))
    {
        g_warning ("Couldn't deserialize JSON object: %s", error->message);
        g_clear_error (&error);
        return;
    }
    g_return_if_fail (login_info != NULL);

    if (!g_variant_lookup (login_info, "domain", "s", &domain))
    {
        g_warning ("Domain is missing");
        goto error;
    }

    if (!g_variant_lookup (login_info, "login", "s", &username))
    {
        g_warning ("Username is missing");
        goto error;
    }

    g_variant_lookup (login_info, "password", "s", &password);

    g_debug ("Got username %s, domain %s", username, domain);

    cookies_in = g_variant_lookup_value (login_info,
                                         "cookies", G_VARIANT_TYPE_DICTIONARY);
    g_variant_builder_init (&builder, (GVariantType *) "a{ss}");
    if (cookies_in)
    {
        GVariantIter iter;
        GVariant *value;
        gchar *key;

        g_debug ("Got cookies");

        g_variant_iter_init (&iter, cookies_in);
        while (g_variant_iter_loop (&iter, "{sv}", &key, &value))
        {
            g_debug ("domain %s, cookies: %s",
                     key, g_variant_get_string (value, NULL));

            g_variant_builder_add (&builder, "{ss}",
                                   key, g_variant_get_string (value, NULL));
        }
        g_variant_unref (cookies_in);
    }

    cookies = g_variant_builder_end (&builder);

    webaccounts_store_login (domain, username, password, cookies);

error:
    g_free (password);
    g_free (username);
    g_free (domain);
    g_variant_unref (login_info);
}
