/*
   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 differnt representations (did vs egoname)
 */

/**
 * @author Tristan Schwieren
 * @file src/did/gnunet-did.c
 * @brief DID Method Wrapper
 *
 */
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_namestore_service.h"
#include "gnunet_gns_service.h"
#include "gnunet_gnsrecord_lib.h"
#include "jansson.h"
#include "did.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 Attribut String
 */
static char *did;

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

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

/**
 * DID Document expiration Date Attribut 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 Callback for ego loockup of get_did_for_ego()
 *
 * @param cls closure
 * @param ego the returned ego
 */
static void
get_did_for_ego_lookup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
  char *did_str;

  if (ego == NULL)
  {
    printf ("EGO not found\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }
  did_str = DID_ego_to_did (ego);

  printf ("%s\n", did_str);

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

/**
 * @brief Get the DID for a given EGO
 *
 */
static void
get_did_for_ego ()
{
  if (egoname != NULL)
  {
    GNUNET_IDENTITY_ego_lookup (my_cfg,
                                egoname,
                                &get_did_for_ego_lookup_cb,
                                NULL);
  }
  else {
    printf ("Set the EGO argument to get the DID for a given EGO\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }
}


/**
 * @brief Get the public key from did attribute given by the user
 *
 * @param pkey place to write the public key to
 */
static void
get_pkey_from_attr_did (struct GNUNET_IDENTITY_PublicKey *pkey)
{
  if (GNUNET_OK != DID_public_key_from_did (did, pkey))
  {
    fprintf (stderr, _("Invalid DID `%s'\n"), did);
    GNUNET_SCHEDULER_add_now (cleanup, NULL);
    ret = 1;
    return;
  }
}

/**
 * @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 (
  void *cls,
  uint32_t rd_count,
  const struct GNUNET_GNSRECORD_Data *rd)
{
  /*
   * FIXME-MSC: The user may decide to put other records here.
   * In general I am fine with the constraint here, but not when
   * we move it to "@"
   */
  if (rd_count != 1)
  {
    printf ("An ego should only have one DID Document\n");
    GNUNET_SCHEDULER_add_now (cleanup, NULL);
    ret = 1;
    return;
  }

  if (rd[0].record_type == GNUNET_DNSPARSER_TYPE_TXT)
  {
    printf ("%s\n", (char *) rd[0].data);
  }
  else {
    printf ("DID Document is not a TXT record\n");
  }

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

/**
 * @brief Resolve a DID given by the user.
 */
static void
resolve_did_document ()
{
  struct GNUNET_IDENTITY_PublicKey pkey;

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

  get_pkey_from_attr_did (&pkey);

  GNUNET_GNS_lookup (gns_handle, GNUNET_GNS_EMPTY_LABEL_AT, &pkey, GNUNET_DNSPARSER_TYPE_TXT,
                     GNUNET_GNS_LO_DEFAULT, &print_did_document, NULL);
}


/**
 * @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, int32_t success, const char *emgs)
{
  struct Event *event;

  if (success != GNUNET_SYSERR)
  {
    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");

    if (emgs != NULL)
    {
      printf ("%s\n", emgs);
    }

    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_IDENTITY_PrivateKey *skey =
    GNUNET_IDENTITY_ego_get_private_key (ego);

  GNUNET_NAMESTORE_records_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 requieres 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);
  }
}


/**
 * @brief Create a did generate did object
 *
 * @param pkey
 * @return void* Return pointer to the DID Document
 */
char *
create_did_generate (struct GNUNET_IDENTITY_PublicKey pkey)
{
  /* FIXME-MSC: I would prefer constants instead of magic numbers */
  char *pkey_str;  // Convert public key to string
  char did_str[71]; // 58 + 12 + 1 = 71
  char *didd_str;
  char verify_id_str[77]; // did_str len + "#key-1" = 71 + 6 = 77
  char *pkey_multibase_str;

  /* FIXME-MSC: This screams for a GNUNET_DID_identity_key_to_string() */
  char *b64;
  char pkx[34];
  pkx[0] = 0xed;
  pkx[1] = 0x01;
  memcpy (pkx + 2, &(pkey.eddsa_key), sizeof(pkey.eddsa_key));
  GNUNET_STRINGS_base64_encode (pkx, sizeof(pkx), &b64);

  GNUNET_asprintf (&pkey_multibase_str, "u%s", b64);

  json_t *didd;
  json_t *did_json;
  json_t *pkey_multibase_json;
  json_t *context_json;
  json_t *context_1_json;
  json_t *context_2_json;
  json_t *verify_json;
  json_t *verify_1_json;
  json_t *verify_1_type_json;
  json_t *verify_1_id_json;
  json_t *verify_relative_ref_json;
  json_t *auth_json;
  json_t *assert_json;

  /* FIXME-MSC: This screams for GNUNET_DID_identity_to_did() */
  pkey_str = GNUNET_IDENTITY_public_key_to_string (&pkey); // Convert public key to string
  sprintf (did_str, "did:reclaim:%s", pkey_str); // Convert the public key to a DID str
  sprintf (verify_id_str, "did:reclaim:%s#key-1", pkey_str); // Convert the public key to a DID str

  // sprintf(pkey_multibase_str, "V%s", pkey_str); // Convert the public key to MultiBase data format

  /* FIXME-MSC: This is effectively creating a DID Document default template for
   * the initial document.
   * Maybe this can be refactored to generate such a template for an identity?
   * Even if higher layers add/modify it, there should probably still be a
   * GNUNET_DID_document_template_from_identity()
   */
  // Create Json Strings
  did_json = json_string (did_str);
  pkey_multibase_json = json_string (pkey_multibase_str);

  context_1_json = json_string ("https://www.w3.org/ns/did/v1");
  context_2_json = json_string (
    "https://w3id.org/security/suites/ed25519-2020/v1");
  verify_1_id_json = json_string (verify_id_str);
  verify_1_type_json = json_string ("Ed25519VerificationKey2020");

  // Add a relative DID URL to reference a verifiation method
  // https://www.w3.org/TR/did-core/#relative-did-urls`
  verify_relative_ref_json = json_string ("#key-1");

  // Create DID Document
  didd = json_object ();

  // Add context
  context_json = json_array ();
  json_array_append (context_json, context_1_json);
  json_array_append (context_json, context_2_json);
  json_object_set (didd, "@context", context_json);

  // Add id
  json_object_set (didd, "id", did_json);

  // Add verification method
  verify_json = json_array ();
  verify_1_json = json_object ();
  json_object_set (verify_1_json, "id", verify_1_id_json);
  json_object_set (verify_1_json, "type", verify_1_type_json);
  json_object_set (verify_1_json, "controller", did_json);
  json_object_set (verify_1_json, "publicKeyMultiBase", pkey_multibase_json);
  json_array_append (verify_json, verify_1_json);
  json_object_set (didd, "verificationMethod", verify_json);

  // Add authentication method
  auth_json = json_array ();
  json_array_append (auth_json, verify_relative_ref_json);
  json_object_set (didd, "authentication", auth_json);

  // Add assertion method to issue a Verifiable Credential
  assert_json = json_array ();
  json_array_append (assert_json, verify_relative_ref_json);
  json_object_set (didd, "assertionMethod", assert_json);

  // Encode DID Document as JSON string
  didd_str = json_dumps (didd, JSON_INDENT (2));
  if (didd_str == NULL)
  {
    printf ("DID Document could not be encoded");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return NULL;
  }

  // TODO: MORE FREEEEEEEE
  /* FIXME-MSC: json_t's are free'd using "json_decref". Also json_t usually
   * keeps a reference counter. Check jansson docs for how to use it.
   * Also: Use valgrind to find leaks.
   */
  free (pkey_multibase_str);
  free (b64);

  free (didd);
  free (did_json);
  free (pkey_multibase_json);
  free (context_json);
  free (context_1_json);
  free (context_2_json);
  free (verify_json);
  free (verify_1_json);
  free (verify_1_type_json);
  free (verify_1_id_json);
  free (auth_json);
  free (assert_json);
  free (verify_relative_ref_json);

  return didd_str;
}

/**
 * @brief Create a DID. Store DID in Namestore cb
 *
 */
static void
create_did_store_cb (void *cls, int32_t success, const char *emsg)
{
  GNUNET_SCHEDULER_add_now (&cleanup, NULL);
  ret = 0;
  return;
}

/**
 * @brief Create a did. Store DID in Namestore
 *
 * @param didd_str String endoced DID Docuement
 * @param ego Identity whos DID Document is stored
 */
static void
create_did_store (char *didd_str, struct GNUNET_IDENTITY_Ego *ego)
{

  struct GNUNET_TIME_Relative expire_time;
  struct GNUNET_GNSRECORD_Data record_data;
  const struct GNUNET_IDENTITY_PrivateKey *skey;

  if (GNUNET_STRINGS_fancy_time_to_relative ((NULL != expire) ?
                                             expire :
                                             GNUNET_DID_DEFAULT_DID_DOCUMENT_EXPIRATION_TIME,
                                             &expire_time) == GNUNET_OK)
  {
    record_data.data = didd_str;
    record_data.expiration_time = expire_time.rel_value_us;
    record_data.data_size = strlen (didd_str) + 1;
    record_data.record_type = GNUNET_GNSRECORD_typename_to_number ("TXT"),
    record_data.flags = GNUNET_GNSRECORD_RF_RELATIVE_EXPIRATION;

    skey = GNUNET_IDENTITY_ego_get_private_key (ego);

    GNUNET_NAMESTORE_records_store (namestore_handle,
                                    skey,
                                    GNUNET_GNS_EMPTY_LABEL_AT,
                                    1, //FIXME what if GNUNET_GNS_EMPTY_LABEL_AT has records
                                    &record_data,
                                    &create_did_store_cb,
                                    NULL);
  }
  else {
    printf ("Failed to read given expiration time\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }
}

/**
 * @brief Create a did ego lockup cb
 *
 * @param cls
 * @param ego
 */
static void
create_did_ego_lockup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
  struct GNUNET_IDENTITY_PublicKey pkey;
  char *didd_str;

  if (ego == NULL)
  {
    printf ("EGO not found\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }

  GNUNET_IDENTITY_ego_get_public_key (ego, &pkey);

  if (ntohl (pkey.type) != GNUNET_GNSRECORD_TYPE_EDKEY)
  {
    printf ("The EGO has to have an EDDSA key pair\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }

  if (didd != NULL)
  {
    printf (
      "DID Docuement is read from \"did-document\" argument (EXPERIMENTAL)\n");
    didd_str = strdup (didd);
  }
  else {
    // Generate DID Docuement from public key
    didd_str = create_did_generate (pkey);
  }

  // Print DID Document to stdout
  printf ("%s\n", didd_str);

  // Store the DID Document
  create_did_store (didd_str, ego);

  // Save DID Document String to GNS
  free (didd_str);
}

/**
 * @brief Create a did document - Create a new identity first
 */
static void
create_did_document_ego_create_cb (void *cls,
                                   const struct GNUNET_IDENTITY_PrivateKey *pk,
                                   const char *emsg)
{

  if (emsg != NULL)
  {
    printf ("%s\n", emsg);
    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
 *
 */
static void
create_did_document ()
{
  if ((egoname != NULL) && (expire != NULL))
  {
    GNUNET_IDENTITY_create (identity_handle,
                            egoname,
                            NULL,
                            GNUNET_IDENTITY_TYPE_EDDSA,
                            &create_did_document_ego_create_cb,
                            egoname);
  }
  else {
    printf (
      "Set the EGO and the Expiration-time argument to create a new DID(-Document)\n");
    GNUNET_SCHEDULER_add_now (&cleanup, NULL);
    ret = 1;
    return;
  }
}


/**
 * @brief Replace a DID Docuemnt. 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 Docuemnt
 *
 */
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)
{
  if (1 == replace)
  {
    replace_did_document ();
  }
  else if (1 == get)
  {
    resolve_did_document ();
  }
  else if (1 == remove_did)
  {
    remove_did_document (NULL, NULL);
  }
  else if (1 == create)
  {
    create_did_document ();
  }
  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 (NULL == name)
    return;
  if ((1 == create) &&
      (0 == strncmp (name, egoname, strlen (egoname))) &&
      (1 != ego_exists))
  {
    fprintf(stderr, "%s already exists!\n", egoname);
    ego_exists = 1;
    return;
  }
  if (1 == show_all)
  {
    did_str = DID_ego_to_did (ego);
    printf ("%s\n", did_str);
    GNUNET_free (did_str);
    return;
  }
  if (1 == show)
  {
    if (0 == strncmp (name, egoname, strlen (egoname)))
    {
      did_str = DID_ego_to_did (ego);
      printf ("%s\n", 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 ('s',
                               "show",
                               gettext_noop ("Show the DID for a given ego"),
                               &show),
    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 ('A',
                               "--show-all",
                               gettext_noop ("Replace the DID Document."),
                               &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 (argc,
                                       argv,
                                       "gnunet-did",
                                       _ (
                                         "Manage Decentralized Identities (DIDs)"),
                                       options,
                                       &run,
                                       NULL))
    return 1;
  else
    return ret;
}
