/*
   This file is part of GNUnet.
   Copyright (C) 2012-2022 GNUnet e.V.

   GNUnet is free software: you can redistribute it and/or modify it
   under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License,
   or (at your option) any later version.

   GNUnet 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
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */

/**
 * FIXME: Do we only want to handle EdDSA identities?
 * TODO: Own GNS record type
 * TODO: Fix overwrite of records in @ if present look for other with same sub
 * TODO. Tests
 * TODO: Move constants to did.h
 * FIXME: Remove and lookup require different representations (did vs egoname)
 */

/**
 * @author Tristan Schwieren
 * @file src/did/gnunet-did.c
 * @brief DID Method Wrapper
 */
#include "did_core.h"

#include "gnunet_util_lib.h"
#include "gnunet_namestore_service.h"
#include "gnunet_identity_service.h"
#include "gnunet_gns_service.h"
#include "gnunet_gnsrecord_lib.h"

#define GNUNET_DID_DEFAULT_DID_DOCUMENT_EXPIRATION_TIME "1d"

/**
 * return value
 */
static int ret;

/**
 * Replace DID Document Flag
 */
static int replace;

/**
 * Remove DID Document Flag
 */
static int remove_did;

/**
 *  Get DID Documement for DID Flag
 */
static int get;

/**
 * Create DID Document Flag
 */
static int create;

/**
 * Show DID for Ego Flag
 */
static int show;

/**
 * Show DID for Ego Flag
 */
static int show_all;

/**
 * DID Attribute String
 */
static char *did;

/**
 * DID Document Attribute String
 */
static char *didd;

/**
 * Ego Attribute String
 */
static char *egoname;

/**
 * DID Document expiration Date Attribute String
 */
static char *expire;

/*
 * Handle to the GNS service
 */
static struct GNUNET_GNS_Handle *gns_handle;

/*
 * Handle to the NAMESTORE service
 */
static struct GNUNET_NAMESTORE_Handle *namestore_handle;

/*
 * Handle to the IDENTITY service
 */
static struct GNUNET_IDENTITY_Handle *identity_handle;


/*
 * The configuration
 */
const static struct GNUNET_CONFIGURATION_Handle *my_cfg;

/**
 * Give ego exists
 */
static int ego_exists = 0;

/**
 * @brief Disconnect and shutdown
 * @param cls closure
 */
static void
cleanup (void *cls)
{
  if (NULL != gns_handle)
    GNUNET_GNS_disconnect (gns_handle);
  if (NULL != namestore_handle)
    GNUNET_NAMESTORE_disconnect (namestore_handle);
  if (NULL != identity_handle)
    GNUNET_IDENTITY_disconnect (identity_handle);

  GNUNET_free (did);
  GNUNET_free (didd);
  GNUNET_free (egoname);
  GNUNET_free (expire);

  GNUNET_SCHEDULER_shutdown ();
}


/**
 * @brief GNS lookup callback. Prints the DID Document to standard out.
 * Fails if there is more than one DID record.
 *
 * @param cls closure
 * @param rd_count number of records in @a rd
 * @param rd the records in the reply
 */
static void
print_did_document (
  enum GNUNET_GenericReturnValue status,
  const char *did_document,
  void *cls
  )
{
  if (GNUNET_OK == status)
    printf ("%s\n", did_document);
  else
    printf ("An error occurred: %s\n", did_document);

  GNUNET_SCHEDULER_add_now (cleanup, NULL);
  ret = 0;
  return;
}


/**
 * @brief Resolve a DID given by the user.
 */
static void
resolve_did ()
{

  if (did == NULL)
  {
    printf ("Set DID option to resolve DID\n");
    GNUNET_SCHEDULER_add_now (cleanup, NULL);
    ret = 1;
    return;
  }

  if (GNUNET_OK != DID_resolve (did, gns_handle, print_did_document, NULL))
  {
    printf ("An error occurred while resoling the DID\n");
    GNUNET_SCHEDULER_add_now (cleanup, NULL);
    ret = 0;
    return;
  }
}


/**
 * @brief Signature of a callback function that is called after a did has been removed
 */
typedef void
(*remove_did_document_callback) (void *cls);

/**
 * @brief A Structure containing a cont and cls. Can be passed as a cls to a callback function
 *
 */
struct Event
{
  remove_did_document_callback cont;
  void *cls;
};

/**
 * @brief Implements the GNUNET_NAMESTORE_ContinuationWithStatus
 * Calls the callback function and cls in the event struct
 *
 * @param cls closure containing the event struct
 * @param success
 * @param emgs
 */
static void
remove_did_document_namestore_cb (void *cls, enum GNUNET_ErrorCode ec)
{
  struct Event *event;

  if (GNUNET_EC_NONE == ec)
  {
    event = (struct Event *) cls;

    if (event->cont != NULL)
    {
      event->cont (event->cls);
      free (event);
    }
    else
    {
      free (event);
      GNUNET_SCHEDULER_add_now (cleanup, NULL);
      ret = 0;
      return;
    }
  }
  else
  {
    printf ("Something went wrong when deleting the DID Document\n");

    printf ("%s\n", GNUNET_ErrorCode_get_hint (ec));

    GNUNET_SCHEDULER_add_now (cleanup, NULL);
    ret = 0;
    return;
  }
}


/**
 * @brief Callback called after the ego has been locked up
 *
 * @param cls closure
 * @param ego the ego returned by the identity service
 */
static void
remove_did_document_ego_lookup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
  const struct GNUNET_CRYPTO_PrivateKey *skey =
    GNUNET_IDENTITY_ego_get_private_key (ego);

  GNUNET_NAMESTORE_record_set_store (namestore_handle,
                                     skey,
                                     GNUNET_GNS_EMPTY_LABEL_AT,
                                     0,
                                     NULL,
                                     &remove_did_document_namestore_cb,
                                     cls);
}


/**
 * @brief Remove a DID Document
 */
static void
remove_did_document (remove_did_document_callback cont, void *cls)
{
  struct Event *event;

  if (egoname == NULL)
  {
    printf ("Remove requires an ego option\n");
    GNUNET_SCHEDULER_add_now (cleanup, NULL);
    ret = 1;
    return;
  }
  else
  {
    event = malloc (sizeof(*event));
    event->cont = cont;
    event->cls = cls;

    GNUNET_IDENTITY_ego_lookup (my_cfg,
                                egoname,
                                &remove_did_document_ego_lookup_cb,
                                (void *) event);
  }
}


// Needed because create_did_ego_lookup_cb() and
// create_did_ego_create_cb() can call each other
static void
create_did_ego_lockup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego);

/**
 * @brief Create a DID(-Document). Called after DID has been created
 * Prints status and the DID.
 *
 */
static void
create_did_cb (enum GNUNET_GenericReturnValue status, void *cls)
{
  if (GNUNET_OK == status)
  {
    printf ("DID has been created.\n%s\n", (char *) cls);
    free (cls);
    ret = 0;
  }
  else
  {
    printf ("An error occurred while creating the DID.\n");
    ret = 1;
  }

  GNUNET_SCHEDULER_add_now (&cleanup, NULL);
  return;
}


/**
 * @brief Create a DID(-Document) - Called after a new Identity has been created.
 */
static void
create_did_ego_create_cb (void *cls,
                          const struct GNUNET_CRYPTO_PrivateKey *pk,
                          enum GNUNET_ErrorCode ec)
{
  if (GNUNET_EC_NONE != ec)
  {
    printf ("%s\n", GNUNET_ErrorCode_get_hint (ec));
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }

  GNUNET_IDENTITY_ego_lookup (my_cfg,
                              egoname,
                              &create_did_ego_lockup_cb,
                              NULL);
}


/**
 * @brief Create a DID(-Document). Called after ego lookup
 *
 */
static void
create_did_ego_lockup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
  if (ego == NULL)
  {
    // If Ego was not found. Create new one first
    printf ("Ego was not found. Creating new one.\n");
    GNUNET_IDENTITY_create (identity_handle,
                            egoname,
                            NULL,
                            GNUNET_PUBLIC_KEY_TYPE_EDDSA,
                            &create_did_ego_create_cb,
                            egoname);
  }
  else
  {
    char *did_tmp = DID_identity_to_did (ego);
    void *cls_tmp = GNUNET_malloc (strlen (did_tmp) + 1);
    struct GNUNET_TIME_Relative expire_relative;

    if (expire == NULL)
    {
      GNUNET_assert (GNUNET_OK ==
                     GNUNET_STRINGS_fancy_time_to_relative (
                       DID_DOCUMENT_DEFAULT_EXPIRATION_TIME,
                       &expire_relative));
    }
    else if (GNUNET_OK != GNUNET_STRINGS_fancy_time_to_relative (expire,
                                                                 &
                                                                 expire_relative))
    {
      printf ("Failed to read given expiration time\n");
      GNUNET_SCHEDULER_add_now (cleanup, NULL);
      ret = 1;
      GNUNET_free (cls_tmp);
      return;
    }

    strcpy (cls_tmp, did_tmp);
    // TODO: Add DID_document argument
    if (GNUNET_OK != DID_create (ego,
                                 NULL,
                                 &expire_relative,
                                 namestore_handle,
                                 create_did_cb,
                                 cls_tmp))
    {
      printf ("An error occurred while creating the DID.\n");
      ret = 1;
      GNUNET_SCHEDULER_add_now (&cleanup, NULL);
      GNUNET_free (cls_tmp);
      return;
    }
    GNUNET_free (cls_tmp);
  }
}


/**
 * @brief Create a DID(-Document).
 *
 */
static void
create_did ()
{
  // Ego name to be set
  if (egoname == NULL)
  {
    printf ("Set the Ego argument to create a new DID(-Document)\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }

  GNUNET_IDENTITY_ego_lookup (my_cfg,
                              egoname,
                              &create_did_ego_lockup_cb,
                              NULL);
}


/**
 * @brief Replace a DID Document. Callback function after ego lockup
 *
 * @param cls
 * @param ego
 */
static void
replace_did_document_ego_lookup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
  // create_did_store (didd, ego);
}


/**
 * @brief Replace a DID Document. Callback functiona after remove
 *
 * @param cls
 */
static void
replace_did_document_remove_cb (void *cls)
{
  GNUNET_IDENTITY_ego_lookup (my_cfg,
                              egoname,
                              &replace_did_document_ego_lookup_cb,
                              NULL);
}


/**
 * @brief Replace a DID Document
 *
 */
static void
replace_did_document ()
{
  if ((didd != NULL) && (expire != NULL))
  {
    remove_did_document (&replace_did_document_remove_cb, NULL);
  }
  else
  {
    printf (
      "Set the DID Document and expiration time argument to replace the DID Document\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }
}


static void
post_ego_iteration (void *cls)
{
  // TODO: Check that only one argument is set

  if (1 == replace)
  {
    replace_did_document ();
  }
  else if (1 == get)
  {
    resolve_did ();
  }
  else if (1 == remove_did)
  {
    remove_did_document (NULL, NULL);
  }
  else if (1 == create)
  {
    create_did ();
  }
  else
  {
    // No Argument found
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    return;
  }
}


static void
process_dids (void *cls, struct GNUNET_IDENTITY_Ego *ego,
              void **ctx, const char*name)
{
  char *did_str;

  if (ego == NULL)
  {
    if (1 == ego_exists)
    {
      GNUNET_SCHEDULER_add_now (&cleanup, NULL);
      return;
    }
    GNUNET_SCHEDULER_add_now (&post_ego_iteration, NULL);
    return;
  }

  if (1 == show_all)
  {
    did_str = DID_identity_to_did (ego);
    printf ("%s:\n\t%s\n", name, did_str);
    GNUNET_free (did_str);
    return;
  }
  if (1 == show)
  {
    if (0 == strncmp (name, egoname, strlen (egoname)))
    {
      did_str = DID_identity_to_did (ego);
      printf ("%s:\n\t%s\n", name, did_str);
      GNUNET_free (did_str);
      return;
    }
  }
}


static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  gns_handle = GNUNET_GNS_connect (c);
  namestore_handle = GNUNET_NAMESTORE_connect (c);
  my_cfg = c;

  // check if GNS_handle could connect
  if (gns_handle == NULL)
  {
    ret = 1;
    return;
  }

  // check if NAMESTORE_handle could connect
  if (namestore_handle == NULL)
  {
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }

  identity_handle = GNUNET_IDENTITY_connect (c, &process_dids, NULL);
  if (identity_handle == NULL)
  {
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }
}


int
main (int argc, char *const argv[])
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_flag ('C',
                               "create",
                               gettext_noop (
                                 "Create a DID Document and display its DID"),
                               &create),
    GNUNET_GETOPT_option_flag ('g',
                               "get",
                               gettext_noop (
                                 "Get the DID Document associated with the given DID"),
                               &get),
    GNUNET_GETOPT_option_flag ('r',
                               "remove",
                               gettext_noop (
                                 "Remove the DID"),
                               &remove_did),
    GNUNET_GETOPT_option_flag ('R',
                               "replace",
                               gettext_noop ("Replace the DID Document."),
                               &replace),
    GNUNET_GETOPT_option_flag ('s',
                               "show",
                               gettext_noop ("Show the DID for a given ego"),
                               &show),
    GNUNET_GETOPT_option_flag ('A',
                               "show-all",
                               gettext_noop ("Show egos with DIDs"),
                               &show_all),
    GNUNET_GETOPT_option_string ('d',
                                 "did",
                                 "DID",
                                 gettext_noop (
                                   "The Decentralized Identity (DID)"),
                                 &did),
    GNUNET_GETOPT_option_string ('D',
                                 "did-document",
                                 "JSON",
                                 gettext_noop (
                                   "The DID Document to store in GNUNET"),
                                 &didd),
    GNUNET_GETOPT_option_string ('e',
                                 "ego",
                                 "EGO",
                                 gettext_noop ("The name of the EGO"),
                                 &egoname),
    GNUNET_GETOPT_option_string ('t',
                                 "expiration-time",
                                 "TIME",
                                 gettext_noop (
                                   "The time until the DID Document is going to expire (e.g. 5d)"),
                                 &expire),
    GNUNET_GETOPT_OPTION_END
  };

  if (GNUNET_OK !=
      GNUNET_PROGRAM_run (
        GNUNET_OS_project_data_gnunet (),
        argc,
        argv,
        "gnunet-did",
        "Manage Decentralized Identities (DIDs)",
        options,
        &run,
        NULL))
    return 1;
  else
    return ret;
}
