/*
  This file is part of TALER
  Copyright (C) 2020, 2022 Taler Systems SA

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

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

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file templating_api.c
 * @brief logic to load and complete HTML templates
 * @author Christian Grothoff
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include "taler_util.h"
#include "taler_mhd_lib.h"
#include "taler_templating_lib.h"
#include "mustach.h"
#include "mustach-jansson.h"
#include <gnunet/gnunet_mhd_compat.h>


/**
 * Entry in a key-value array we use to cache templates.
 */
struct TVE
{
  /**
   * A name, used as the key. NULL for the last entry.
   */
  char *name;

  /**
   * Language the template is in.
   */
  char *lang;

  /**
   * 0-terminated (!) file data to return for @e name and @e lang.
   */
  char *value;

};


/**
 * Array of templates loaded into RAM.
 */
static struct TVE *loaded;

/**
 * Length of the #loaded array.
 */
static unsigned int loaded_length;


/**
 * Load Mustach template into memory.  Note that we intentionally cache
 * failures, that is if we ever failed to load a template, we will never try
 * again.
 *
 * @param connection the connection we act upon
 * @param name name of the template file to load
 *        (MUST be a 'static' string in memory!)
 * @return NULL on error, otherwise the template
 */
static const char *
lookup_template (struct MHD_Connection *connection,
                 const char *name)
{
  struct TVE *best = NULL;
  double best_q = 0.0;
  const char *lang;

  lang = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
  if (NULL == lang)
    lang = "en";
  /* find best match by language */
  for (unsigned int i = 0; i<loaded_length; i++)
  {
    double q;

    if (0 != strcmp (loaded[i].name,
                     name))
      continue; /* does not match by name */
    if (NULL == loaded[i].lang) /* no language == always best match */
      return loaded[i].value;
    q = TALER_pattern_matches (lang,
                               loaded[i].lang);
    if (q < best_q)
      continue;
    best_q = q;
    best = &loaded[i];
  }
  if (NULL == best)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "No templates found for `%s'\n",
                name);
    return NULL;
  }
  return best->value;
}


/**
 * Get the base URL for static resources.
 *
 * @param con the MHD connection
 * @param instance_id the instance ID
 * @returns the static files base URL, guaranteed
 *          to have a trailing slash.
 */
static char *
make_static_url (struct MHD_Connection *con,
                 const char *instance_id)
{
  const char *host;
  const char *forwarded_host;
  const char *uri_path;
  struct GNUNET_Buffer buf = { 0 };

  host = MHD_lookup_connection_value (con,
                                      MHD_HEADER_KIND,
                                      "Host");
  forwarded_host = MHD_lookup_connection_value (con,
                                                MHD_HEADER_KIND,
                                                "X-Forwarded-Host");

  uri_path = MHD_lookup_connection_value (con,
                                          MHD_HEADER_KIND,
                                          "X-Forwarded-Prefix");
  if (NULL != forwarded_host)
    host = forwarded_host;

  if (NULL == host)
  {
    GNUNET_break (0);
    return NULL;
  }

  GNUNET_assert (NULL != instance_id);

  if (GNUNET_NO == TALER_mhd_is_https (con))
    GNUNET_buffer_write_str (&buf,
                             "http://");
  else
    GNUNET_buffer_write_str (&buf,
                             "https://");
  GNUNET_buffer_write_str (&buf,
                           host);
  if (NULL != uri_path)
    GNUNET_buffer_write_path (&buf,
                              uri_path);
  if (0 != strcmp ("default",
                   instance_id))
  {
    GNUNET_buffer_write_path (&buf,
                              "instances");
    GNUNET_buffer_write_path (&buf,
                              instance_id);
  }
  GNUNET_buffer_write_path (&buf,
                            "static/");
  return GNUNET_buffer_reap_str (&buf);
}


int
TALER_TEMPLATING_fill (const char *tmpl,
                       const json_t *root,
                       void **result,
                       size_t *result_size)
{
  int eno;

  if (0 !=
      (eno = mustach_jansson_mem (tmpl,
                                  0, /* length of tmpl */
                                  (json_t *) root,
                                  Mustach_With_AllExtensions,
                                  (char **) result,
                                  result_size)))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "mustach failed on template with error %d\n",
                eno);
    *result = NULL;
    *result_size = 0;
    return eno;
  }
  return eno;
}


int
TALER_TEMPLATING_fill2 (const void *tmpl,
                        size_t tmpl_len,
                        const json_t *root,
                        void **result,
                        size_t *result_size)
{
  int eno;

  if (0 !=
      (eno = mustach_jansson_mem (tmpl,
                                  tmpl_len,
                                  (json_t *) root,
                                  Mustach_With_AllExtensions,
                                  (char **) result,
                                  result_size)))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "mustach failed on template with error %d\n",
                eno);
    *result = NULL;
    *result_size = 0;
    return eno;
  }
  return eno;
}


enum GNUNET_GenericReturnValue
TALER_TEMPLATING_build (struct MHD_Connection *connection,
                        unsigned int *http_status,
                        const char *template,
                        const char *instance_id,
                        const char *taler_uri,
                        const json_t *root,
                        struct MHD_Response **reply)
{
  char *body;
  size_t body_size;

  {
    const char *tmpl;
    int eno;

    tmpl = lookup_template (connection,
                            template);
    if (NULL == tmpl)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to load template `%s'\n",
                  template);
      *http_status = MHD_HTTP_NOT_ACCEPTABLE;
      *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE,
                                     template);
      return GNUNET_NO;
    }
    /* Add default values to the context */
    if (NULL != instance_id)
    {
      char *static_url = make_static_url (connection,
                                          instance_id);

      GNUNET_break (0 ==
                    json_object_set_new ((json_t *) root,
                                         "static_url",
                                         json_string (static_url)));
      GNUNET_free (static_url);
    }
    if (0 !=
        (eno = mustach_jansson_mem (tmpl,
                                    0,
                                    (json_t *) root,
                                    Mustach_With_NoExtensions,
                                    &body,
                                    &body_size)))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "mustach failed on template `%s' with error %d\n",
                  template,
                  eno);
      *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
      *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE,
                                     template);
      return GNUNET_NO;
    }
  }

  /* try to compress reply if client allows it */
  {
    bool compressed = false;

    if (MHD_YES ==
        TALER_MHD_can_compress (connection))
    {
      compressed = TALER_MHD_body_compress ((void **) &body,
                                            &body_size);
    }
    *reply = MHD_create_response_from_buffer (body_size,
                                              body,
                                              MHD_RESPMEM_MUST_FREE);
    if (NULL == *reply)
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
    if (compressed)
    {
      if (MHD_NO ==
          MHD_add_response_header (*reply,
                                   MHD_HTTP_HEADER_CONTENT_ENCODING,
                                   "deflate"))
      {
        GNUNET_break (0);
        MHD_destroy_response (*reply);
        *reply = NULL;
        return GNUNET_SYSERR;
      }
    }
  }

  /* Add standard headers */
  if (NULL != taler_uri)
    GNUNET_break (MHD_NO !=
                  MHD_add_response_header (*reply,
                                           "Taler",
                                           taler_uri));
  GNUNET_break (MHD_NO !=
                MHD_add_response_header (*reply,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         "text/html"));
  return GNUNET_OK;
}


enum GNUNET_GenericReturnValue
TALER_TEMPLATING_reply (struct MHD_Connection *connection,
                        unsigned int http_status,
                        const char *template,
                        const char *instance_id,
                        const char *taler_uri,
                        const json_t *root)
{
  enum GNUNET_GenericReturnValue res;
  struct MHD_Response *reply;
  MHD_RESULT ret;

  res = TALER_TEMPLATING_build (connection,
                                &http_status,
                                template,
                                instance_id,
                                taler_uri,
                                root,
                                &reply);
  if (GNUNET_SYSERR == res)
    return res;
  ret = MHD_queue_response (connection,
                            http_status,
                            reply);
  MHD_destroy_response (reply);
  if (MHD_NO == ret)
    return GNUNET_SYSERR;
  return (res == GNUNET_OK)
    ? GNUNET_OK
    : GNUNET_NO;
}


/**
 * Function called with a template's filename.
 *
 * @param cls closure, NULL
 * @param filename complete filename (absolute path)
 * @return #GNUNET_OK to continue to iterate,
 *  #GNUNET_NO to stop iteration with no error,
 *  #GNUNET_SYSERR to abort iteration with error!
 */
static enum GNUNET_GenericReturnValue
load_template (void *cls,
               const char *filename)
{
  char *lang;
  char *end;
  int fd;
  struct stat sb;
  char *map;
  const char *name;

  (void) cls;
  if ('.' == filename[0])
    return GNUNET_OK;
  name = strrchr (filename,
                  '/');
  if (NULL == name)
    name = filename;
  else
    name++;
  lang = strchr (name,
                 '.');
  if (NULL == lang)
    return GNUNET_OK; /* name must include .$LANG */
  lang++;
  end = strchr (lang,
                '.');
  if ( (NULL == end) ||
       (0 != strcmp (end,
                     ".must")) )
    return GNUNET_OK; /* name must end with '.must' */

  /* finally open template */
  fd = open (filename,
             O_RDONLY);
  if (-1 == fd)
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                              "open",
                              filename);

    return GNUNET_SYSERR;
  }
  if (0 !=
      fstat (fd,
             &sb))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                              "fstat",
                              filename);
    GNUNET_break (0 == close (fd));
    return GNUNET_OK;
  }
  map = GNUNET_malloc_large (sb.st_size + 1);
  if (NULL == map)
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "malloc");
    GNUNET_break (0 == close (fd));
    return GNUNET_SYSERR;
  }
  if (sb.st_size !=
      read (fd,
            map,
            sb.st_size))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                              "read",
                              filename);
    GNUNET_break (0 == close (fd));
    return GNUNET_OK;
  }
  GNUNET_break (0 == close (fd));
  GNUNET_array_grow (loaded,
                     loaded_length,
                     loaded_length + 1);
  loaded[loaded_length - 1].name = GNUNET_strndup (name,
                                                   (lang - 1) - name);
  loaded[loaded_length - 1].lang = GNUNET_strndup (lang,
                                                   end - lang);
  loaded[loaded_length - 1].value = map;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Loading template `%s' (%s)\n",
              filename,
              loaded[loaded_length - 1].name);
  return GNUNET_OK;
}


MHD_RESULT
TALER_TEMPLATING_reply_error (
  struct MHD_Connection *connection,
  const char *template_basename,
  unsigned int http_status,
  enum TALER_ErrorCode ec,
  const char *detail)
{
  json_t *data;
  enum GNUNET_GenericReturnValue ret;

  data = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_uint64 ("ec",
                             ec),
    GNUNET_JSON_pack_string ("hint",
                             TALER_ErrorCode_get_hint (ec)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("detail",
                               detail))
    );
  ret = TALER_TEMPLATING_reply (connection,
                                http_status,
                                template_basename,
                                NULL,
                                NULL,
                                data);
  json_decref (data);
  switch (ret)
  {
  case GNUNET_OK:
    return MHD_YES;
  case GNUNET_NO:
    return MHD_YES;
  case GNUNET_SYSERR:
    return MHD_NO;
  }
  GNUNET_assert (0);
  return MHD_NO;
}


enum GNUNET_GenericReturnValue
TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd)
{
  char *dn;
  int ret;

  {
    char *path;

    path = GNUNET_OS_installation_get_path (pd,
                                            GNUNET_OS_IPK_DATADIR);
    if (NULL == path)
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
    GNUNET_asprintf (&dn,
                     "%s/templates/",
                     path);
    GNUNET_free (path);
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Loading templates from `%s'\n",
              dn);
  ret = GNUNET_DISK_directory_scan (dn,
                                    &load_template,
                                    NULL);
  GNUNET_free (dn);
  if (-1 == ret)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


void
TALER_TEMPLATING_done (void)
{
  for (unsigned int i = 0; i<loaded_length; i++)
  {
    GNUNET_free (loaded[i].name);
    GNUNET_free (loaded[i].lang);
    GNUNET_free (loaded[i].value);
  }
  GNUNET_array_grow (loaded,
                     loaded_length,
                     0);
}


/* end of templating_api.c */
