/*
 * empathy-auth-goa.c - Source for Goa SASL authentication
 * Copyright (C) 2011 Collabora Ltd.
 * @author Xavier Claessens <xavier.claessens@collabora.co.uk>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"

#define GOA_API_IS_SUBJECT_TO_CHANGE /* awesome! */
#include <goa/goa.h>

#include <libsoup/soup.h>
#include <string.h>

#define DEBUG_FLAG EMPATHY_DEBUG_SASL
#include "empathy-debug.h"
#include "empathy-utils.h"
#include "empathy-goa-auth-handler.h"

#define MECH_FACEBOOK "X-FACEBOOK-PLATFORM"
#define MECH_MSN "X-MESSENGER-OAUTH2"

static const gchar *supported_mechanisms[] = {
    MECH_FACEBOOK,
    MECH_MSN,
    NULL};

struct _EmpathyGoaAuthHandlerPriv
{
  GoaClient *client;
  gboolean client_preparing;

  /* List of AuthData waiting for client to be created */
  GList *auth_queue;
};

G_DEFINE_TYPE (EmpathyGoaAuthHandler, empathy_goa_auth_handler, G_TYPE_OBJECT);

static void
empathy_goa_auth_handler_init (EmpathyGoaAuthHandler *self)
{
  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
      EMPATHY_TYPE_GOA_AUTH_HANDLER, EmpathyGoaAuthHandlerPriv);
}

static void
empathy_goa_auth_handler_dispose (GObject *object)
{
  EmpathyGoaAuthHandler *self = (EmpathyGoaAuthHandler *) object;

  /* AuthData keeps a ref on self */
  g_assert (self->priv->auth_queue == NULL);

  tp_clear_object (&self->priv->client);

  G_OBJECT_CLASS (empathy_goa_auth_handler_parent_class)->dispose (object);
}

static void
empathy_goa_auth_handler_class_init (EmpathyGoaAuthHandlerClass *klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);

  oclass->dispose = empathy_goa_auth_handler_dispose;

  g_type_class_add_private (klass, sizeof (EmpathyGoaAuthHandlerPriv));
}

EmpathyGoaAuthHandler *
empathy_goa_auth_handler_new (void)
{
  return g_object_new (EMPATHY_TYPE_GOA_AUTH_HANDLER, NULL);
}

typedef struct
{
  EmpathyGoaAuthHandler *self;
  TpChannel *channel;
  TpAccount *account;

  GoaObject *goa_object;
  gchar *access_token;
} AuthData;

static void
auth_data_free (AuthData *data)
{
  tp_clear_object (&data->self);
  tp_clear_object (&data->channel);
  tp_clear_object (&data->account);
  tp_clear_object (&data->goa_object);
  g_free (data->access_token);
  g_slice_free (AuthData, data);
}

static void
fail_auth (AuthData *data)
{
  DEBUG ("Auth failed for account %s",
      tp_proxy_get_object_path (data->account));

  tp_channel_close_async (data->channel, NULL, NULL);
  auth_data_free (data);
}

static void
sasl_status_changed_cb (TpChannel *channel,
    guint status,
    const gchar *reason,
    GHashTable *details,
    gpointer user_data,
    GObject *self)
{
  switch (status)
    {
      case TP_SASL_STATUS_SERVER_SUCCEEDED:
        tp_cli_channel_interface_sasl_authentication_call_accept_sasl (channel,
            -1, NULL, NULL, NULL, NULL);
        break;

      case TP_SASL_STATUS_SUCCEEDED:
      case TP_SASL_STATUS_SERVER_FAILED:
      case TP_SASL_STATUS_CLIENT_FAILED:
        tp_cli_channel_call_close (channel, -1, NULL, NULL, NULL, NULL);
        break;

      default:
        break;
    }
}

static void
facebook_new_challenge_cb (TpChannel *channel,
    const GArray *challenge,
    gpointer user_data,
    GObject *weak_object)
{
  AuthData *data = user_data;
  GoaOAuth2Based *oauth2;
  const gchar *client_id;
  GHashTable *h;
  GHashTable *params;
  gchar *response;
  GArray *response_array;

  DEBUG ("new challenge for %s:\n%s",
      tp_proxy_get_object_path (data->account),
      challenge->data);

  h = soup_form_decode (challenge->data);

  oauth2 = goa_object_get_oauth2_based (data->goa_object);
  client_id = goa_oauth2_based_get_client_id (oauth2);

  /* See https://developers.facebook.com/docs/chat/#platauth */
  params = g_hash_table_new (g_str_hash, g_str_equal);
  g_hash_table_insert (params, "method", g_hash_table_lookup (h, "method"));
  g_hash_table_insert (params, "nonce", g_hash_table_lookup (h, "nonce"));
  g_hash_table_insert (params, "access_token", data->access_token);
  g_hash_table_insert (params, "api_key", (gpointer) client_id);
  g_hash_table_insert (params, "call_id", "0");
  g_hash_table_insert (params, "v", "1.0");

  response = soup_form_encode_hash (params);
  DEBUG ("Response: %s", response);

  response_array = g_array_new (FALSE, FALSE, sizeof (gchar));
  g_array_append_vals (response_array, response, strlen (response));

  tp_cli_channel_interface_sasl_authentication_call_respond (data->channel, -1,
      response_array, NULL, NULL, NULL, NULL);

  g_hash_table_unref (h);
  g_hash_table_unref (params);
  g_object_unref (oauth2);
  g_free (response);
  g_array_unref (response_array);
}

static void
got_oauth2_access_token_cb (GObject *source,
    GAsyncResult *result,
    gpointer user_data)
{
  GoaOAuth2Based *oauth2 = (GoaOAuth2Based *) source;
  AuthData *data = user_data;
  gint expires_in;
  GError *error = NULL;

  if (!goa_oauth2_based_call_get_access_token_finish (oauth2,
      &data->access_token, &expires_in, result, &error))
    {
      DEBUG ("Failed to get access token: %s", error->message);
      fail_auth (data);
      g_clear_error (&error);
      return;
    }

  DEBUG ("Got access token for %s:\n%s",
      tp_proxy_get_object_path (data->account),
      data->access_token);

  tp_cli_channel_interface_sasl_authentication_connect_to_sasl_status_changed (
      data->channel, sasl_status_changed_cb, NULL, NULL, NULL, NULL);
  g_assert_no_error (error);

  if (empathy_sasl_channel_supports_mechanism (data->channel, MECH_FACEBOOK))
    {
      /* Give ownership of data to signal connection */
      tp_cli_channel_interface_sasl_authentication_connect_to_new_challenge (
          data->channel, facebook_new_challenge_cb,
          data, (GDestroyNotify) auth_data_free,
          NULL, NULL);

      DEBUG ("Start %s mechanism for account %s", MECH_FACEBOOK,
          tp_proxy_get_object_path (data->account));

      tp_cli_channel_interface_sasl_authentication_call_start_mechanism (
          data->channel, -1, MECH_FACEBOOK, NULL, NULL, NULL, NULL);
    }
  else if (empathy_sasl_channel_supports_mechanism (data->channel, MECH_MSN))
    {
      guchar *token_decoded;
      gsize token_decoded_len;
      GArray *token_decoded_array;

      /* Wocky will base64 encode, but token actually already is base64, so we
       * decode now and it will be re-encoded. */
      token_decoded = g_base64_decode (data->access_token, &token_decoded_len);
      token_decoded_array = g_array_new (FALSE, FALSE, sizeof (guchar));
      g_array_append_vals (token_decoded_array, token_decoded, token_decoded_len);

      DEBUG ("Start %s mechanism for account %s", MECH_MSN,
          tp_proxy_get_object_path (data->account));

      tp_cli_channel_interface_sasl_authentication_call_start_mechanism_with_data (
          data->channel, -1, MECH_MSN, token_decoded_array,
          NULL, NULL, NULL, NULL);

      g_array_unref (token_decoded_array);
      g_free (token_decoded);
      auth_data_free (data);
    }
  else
    {
      /* We already checked it supports one of supported_mechanisms, so this
       * can't happen */
      g_assert_not_reached ();
    }
}

static void
ensure_credentials_cb (GObject *source,
    GAsyncResult *result,
    gpointer user_data)
{
  AuthData *data = user_data;
  GoaAccount *goa_account = (GoaAccount *) source;
  GoaOAuth2Based *oauth2;
  gint expires_in;
  GError *error = NULL;

  if (!goa_account_call_ensure_credentials_finish (goa_account, &expires_in,
      result, &error))
    {
      DEBUG ("Failed to EnsureCredentials: %s", error->message);
      fail_auth (data);
      g_clear_error (&error);
      return;
    }

  /* We support only oaut2 */
  oauth2 = goa_object_get_oauth2_based (data->goa_object);
  if (oauth2 == NULL)
    {
      DEBUG ("GoaObject does not implement oauth2");
      fail_auth (data);
      return;
    }

  DEBUG ("Goa daemon has credentials for %s, get the access token",
      tp_proxy_get_object_path (data->account));

  goa_oauth2_based_call_get_access_token (oauth2, NULL,
      got_oauth2_access_token_cb, data);

  g_object_unref (oauth2);
}

static void
start_auth (AuthData *data)
{
  EmpathyGoaAuthHandler *self = data->self;
  const GValue *id_value;
  const gchar *id;
  GList *goa_accounts, *l;
  gboolean found = FALSE;

  id_value = tp_account_get_storage_identifier (data->account);
  id = g_value_get_string (id_value);

  goa_accounts = goa_client_get_accounts (self->priv->client);
  for (l = goa_accounts; l != NULL && !found; l = l->next)
    {
      GoaObject *goa_object = l->data;
      GoaAccount *goa_account;

      goa_account = goa_object_get_account (goa_object);
      if (!tp_strdiff (goa_account_get_id (goa_account), id))
        {
          data->goa_object = g_object_ref (goa_object);

          DEBUG ("Found the GoaAccount for %s, ensure credentials",
              tp_proxy_get_object_path (data->account));

          goa_account_call_ensure_credentials (goa_account, NULL,
              ensure_credentials_cb, data);

          found = TRUE;
        }

      g_object_unref (goa_account);
    }
  g_list_free_full (goa_accounts, g_object_unref);

  if (!found)
    {
      DEBUG ("Cannot find GoaAccount");
      fail_auth (data);
    }
}

static void
client_new_cb (GObject *source,
    GAsyncResult *result,
    gpointer user_data)
{
  EmpathyGoaAuthHandler *self = user_data;
  GList *l;
  GError *error = NULL;

  self->priv->client_preparing = FALSE;
  self->priv->client = goa_client_new_finish (result, &error);
  if (self->priv->client == NULL)
    {
      DEBUG ("Error getting GoaClient: %s", error->message);
      g_clear_error (&error);
    }

  /* process queued data */
  for (l = self->priv->auth_queue; l != NULL; l = l->next)
    {
      AuthData *data = l->data;

      if (self->priv->client != NULL)
        start_auth (data);
      else
        fail_auth (data);
    }

  tp_clear_pointer (&self->priv->auth_queue, g_list_free);
}

void
empathy_goa_auth_handler_start (EmpathyGoaAuthHandler *self,
    TpChannel *channel,
    TpAccount *account)
{
  AuthData *data;

  g_return_if_fail (TP_IS_CHANNEL (channel));
  g_return_if_fail (TP_IS_ACCOUNT (account));
  g_return_if_fail (empathy_goa_auth_handler_supports (self, channel, account));

  DEBUG ("Start Goa auth for account: %s",
      tp_proxy_get_object_path (account));

  data = g_slice_new0 (AuthData);
  data->self = g_object_ref (self);
  data->channel = g_object_ref (channel);
  data->account = g_object_ref (account);

  if (self->priv->client == NULL)
    {
      /* GOA client not ready yet, queue data */
      if (!self->priv->client_preparing)
        {
          goa_client_new (NULL, client_new_cb, self);
          self->priv->client_preparing = TRUE;
        }

      self->priv->auth_queue = g_list_prepend (self->priv->auth_queue, data);
    }
  else
    {
      start_auth (data);
    }
}

gboolean
empathy_goa_auth_handler_supports (EmpathyGoaAuthHandler *self,
    TpChannel *channel,
    TpAccount *account)
{
  const gchar *provider;
  const gchar * const *iter;

  g_return_val_if_fail (TP_IS_CHANNEL (channel), FALSE);
  g_return_val_if_fail (TP_IS_ACCOUNT (account), FALSE);

  provider = tp_account_get_storage_provider (account);
  if (tp_strdiff (provider, EMPATHY_GOA_PROVIDER))
    return FALSE;

  for (iter = supported_mechanisms; *iter != NULL; iter++)
    {
      if (empathy_sasl_channel_supports_mechanism (channel, *iter))
        return TRUE;
    }

  return FALSE;
}