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

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

  You should have received a copy of the GNU Affero General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file mhd_legal.c
 * @brief API for returning legal documents based on client language
 *        and content type preferences
 * @author Christian Grothoff
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_json_lib.h>
#include <jansson.h>
#include <microhttpd.h>
#include "taler_util.h"
#include "taler_mhd_lib.h"

/**
 * How long should browsers/proxies cache the "legal" replies?
 */
#define MAX_TERMS_CACHING GNUNET_TIME_UNIT_DAYS

/**
 * HTTP header with the version of the terms of service.
 */
#define TALER_TERMS_VERSION "Taler-Terms-Version"

/**
 * Entry in the terms-of-service array.
 */
struct Terms
{
  /**
   * Kept in a DLL.
   */
  struct Terms *prev;

  /**
   * Kept in a DLL.
   */
  struct Terms *next;

  /**
   * Mime type of the terms.
   */
  const char *mime_type;

  /**
   * The terms (NOT 0-terminated!), mmap()'ed. Do not free,
   * use munmap() instead.
   */
  void *terms;

  /**
   * The desired language.
   */
  char *language;

  /**
   * deflated @e terms, to return if client supports deflate compression.
   * malloc()'ed.  NULL if @e terms does not compress.
   */
  void *compressed_terms;

  /**
   * Etag we use for this response.
   */
  char *terms_etag;

  /**
   * Number of bytes in @e terms.
   */
  size_t terms_size;

  /**
   * Number of bytes in @e compressed_terms.
   */
  size_t compressed_terms_size;

  /**
   * Sorting key by format preference in case
   * everything else is equal. Higher is preferred.
   */
  unsigned int priority;

};


/**
 * Prepared responses for legal documents
 * (terms of service, privacy policy).
 */
struct TALER_MHD_Legal
{
  /**
   * DLL of terms of service.
   */
  struct Terms *terms_head;

  /**
   * DLL of terms of service.
   */
  struct Terms *terms_tail;

  /**
   * Etag to use for the terms of service (= version).
   */
  char *terms_version;
};


MHD_RESULT
TALER_MHD_reply_legal (struct MHD_Connection *conn,
                       struct TALER_MHD_Legal *legal)
{
  /* Default terms of service if none are configured */
  static struct Terms none = {
    .mime_type = "text/plain",
    .terms = (void *) "not configured",
    .language = (void *) "en",
    .terms_size = strlen ("not configured")
  };
  struct MHD_Response *resp;
  struct Terms *t;
  struct GNUNET_TIME_Absolute a;
  struct GNUNET_TIME_Timestamp m;
  char dat[128];
  char *langs;

  t = NULL;
  langs = NULL;

  a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING);
  m = GNUNET_TIME_absolute_to_timestamp (a);
  /* Round up to next full day to ensure the expiration
     time does not become a fingerprint! */
  a = GNUNET_TIME_absolute_round_down (a,
                                       MAX_TERMS_CACHING);
  a = GNUNET_TIME_absolute_add (a,
                                MAX_TERMS_CACHING);
  TALER_MHD_get_date_string (m.abs_time,
                             dat);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Setting '%s' header to '%s'\n",
              MHD_HTTP_HEADER_EXPIRES,
              dat);
  if (NULL == legal)
  {
    t = &none;
    goto return_t;
  }

  if (NULL != legal)
  {
    const char *mime;
    const char *lang;
    double best_mime_q = 0.0;
    double best_lang_q = 0.0;

    mime = MHD_lookup_connection_value (conn,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_ACCEPT);
    if (NULL == mime)
      mime = "text/plain";
    lang = MHD_lookup_connection_value (conn,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    if (NULL == lang)
      lang = "en";
    /* Find best match: must match mime type (if possible), and if
       mime type matches, ideally also language */
    for (struct Terms *p = legal->terms_head;
         NULL != p;
         p = p->next)
    {
      double q;

      q = TALER_pattern_matches (mime,
                                 p->mime_type);
      if (q > best_mime_q)
        best_mime_q = q;
    }
    for (struct Terms *p = legal->terms_head;
         NULL != p;
         p = p->next)
    {
      double q;

      q = TALER_pattern_matches (mime,
                                 p->mime_type);
      if (q < best_mime_q)
        continue;
      q = TALER_pattern_matches (lang,
                                 p->language);
      /* create 'available-languages' (for this mime-type) */
      if (NULL == langs)
      {
        langs = GNUNET_strdup (p->language);
      }
      else if (NULL == strstr (langs,
                               p->language))
      {
        char *tmp = langs;

        GNUNET_asprintf (&langs,
                         "%s,%s",
                         tmp,
                         p->language);
        GNUNET_free (tmp);
      }
      if (q < best_lang_q)
        continue;
      best_lang_q = q;
      t = p;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Best match for %s/%s: %s / %s\n",
                lang,
                mime,
                (NULL != t) ? t->mime_type : "<none>",
                (NULL != t) ? t->language : "<none>");
  }

  if (NULL != t)
  {
    const char *etag;

    etag = MHD_lookup_connection_value (conn,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_IF_NONE_MATCH);
    if ( (NULL != etag) &&
         (NULL != t->terms_etag) &&
         (0 == strcasecmp (etag,
                           t->terms_etag)) )
    {
      MHD_RESULT ret;

      resp = MHD_create_response_from_buffer (0,
                                              NULL,
                                              MHD_RESPMEM_PERSISTENT);
      TALER_MHD_add_global_headers (resp);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_EXPIRES,
                                             dat));
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_ETAG,
                                             t->terms_etag));
      if (NULL != legal)
        GNUNET_break (MHD_YES ==
                      MHD_add_response_header (resp,
                                               TALER_TERMS_VERSION,
                                               legal->terms_version));
      ret = MHD_queue_response (conn,
                                MHD_HTTP_NOT_MODIFIED,
                                resp);
      GNUNET_break (MHD_YES == ret);
      MHD_destroy_response (resp);
      return ret;
    }
  }

  if (NULL == t)
    t = &none; /* 501 if not configured */

return_t:
  /* try to compress the response */
  resp = NULL;
  if ( (MHD_YES ==
        TALER_MHD_can_compress (conn)) &&
       (NULL != t->compressed_terms) )
  {
    resp = MHD_create_response_from_buffer (t->compressed_terms_size,
                                            t->compressed_terms,
                                            MHD_RESPMEM_PERSISTENT);
    if (MHD_NO ==
        MHD_add_response_header (resp,
                                 MHD_HTTP_HEADER_CONTENT_ENCODING,
                                 "deflate"))
    {
      GNUNET_break (0);
      MHD_destroy_response (resp);
      resp = NULL;
    }
  }
  if (NULL == resp)
  {
    /* could not generate compressed response, return uncompressed */
    resp = MHD_create_response_from_buffer (t->terms_size,
                                            (void *) t->terms,
                                            MHD_RESPMEM_PERSISTENT);
  }
  TALER_MHD_add_global_headers (resp);
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (resp,
                                         MHD_HTTP_HEADER_EXPIRES,
                                         dat));
  if (NULL != langs)
  {
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (resp,
                                           "Avail-Languages",
                                           langs));
    GNUNET_free (langs);
  }
  /* Set cache control headers: our response varies depending on these headers */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (resp,
                                         MHD_HTTP_HEADER_VARY,
                                         MHD_HTTP_HEADER_ACCEPT_LANGUAGE ","
                                         MHD_HTTP_HEADER_ACCEPT ","
                                         MHD_HTTP_HEADER_ACCEPT_ENCODING));
  /* Information is always public, revalidate after 10 days */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (resp,
                                         MHD_HTTP_HEADER_CACHE_CONTROL,
                                         "public,max-age=864000"));
  if (NULL != t->terms_etag)
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (resp,
                                           MHD_HTTP_HEADER_ETAG,
                                           t->terms_etag));
  if (NULL != legal)
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (resp,
                                           TALER_TERMS_VERSION,
                                           legal->terms_version));
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (resp,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         t->mime_type));
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (resp,
                                         MHD_HTTP_HEADER_CONTENT_LANGUAGE,
                                         t->language));
  {
    MHD_RESULT ret;

    ret = MHD_queue_response (conn,
                              t == &none
                              ? MHD_HTTP_NOT_IMPLEMENTED
                              : MHD_HTTP_OK,
                              resp);
    MHD_destroy_response (resp);
    return ret;
  }
}


/**
 * Load all the terms of service from @a path under language @a lang
 * from file @a name
 *
 * @param[in,out] legal where to write the result
 * @param path where the terms are found
 * @param lang which language directory to crawl
 * @param name specific file to access
 */
static void
load_terms (struct TALER_MHD_Legal *legal,
            const char *path,
            const char *lang,
            const char *name)
{
  static struct MimeMap
  {
    const char *ext;
    const char *mime;
    unsigned int priority;
  } mm[] = {
    { .ext = ".txt", .mime = "text/plain", .priority = 150 },
    { .ext = ".html", .mime = "text/html", .priority = 100 },
    { .ext = ".htm", .mime = "text/html", .priority = 99 },
    { .ext = ".md", .mime = "text/markdown", .priority = 50 },
    { .ext = ".pdf", .mime = "application/pdf", .priority = 25 },
    { .ext = ".jpg", .mime = "image/jpeg" },
    { .ext = ".jpeg", .mime = "image/jpeg" },
    { .ext = ".png", .mime = "image/png" },
    { .ext = ".gif", .mime = "image/gif" },
    { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 },
    { .ext = ".xml", .mime = "text/xml", .priority = 10 },
    { .ext = NULL, .mime = NULL }
  };
  const char *ext = strrchr (name, '.');
  const char *mime;
  unsigned int priority;

  if (NULL == ext)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
                name,
                path,
                lang);
    return;
  }
  if ( (NULL == legal->terms_version) ||
       (0 != strncmp (legal->terms_version,
                      name,
                      ext - name - 1)) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n",
                name,
                legal->terms_version,
                path,
                lang);
    return;
  }
  mime = NULL;
  for (unsigned int i = 0; NULL != mm[i].ext; i++)
    if (0 == strcasecmp (mm[i].ext,
                         ext))
    {
      mime = mm[i].mime;
      priority = mm[i].priority;
      break;
    }
  if (NULL == mime)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n",
                ext,
                name,
                path,
                lang);
    return;
  }
  /* try to read the file with the terms of service */
  {
    struct stat st;
    char *fn;
    int fd;

    GNUNET_asprintf (&fn,
                     "%s/%s/%s",
                     path,
                     lang,
                     name);
    fd = open (fn, O_RDONLY);
    if (-1 == fd)
    {
      GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                                "open",
                                fn);
      GNUNET_free (fn);
      return;
    }
    if (0 != fstat (fd, &st))
    {
      GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                                "fstat",
                                fn);
      GNUNET_break (0 == close (fd));
      GNUNET_free (fn);
      return;
    }
    if (SIZE_MAX < ((unsigned long long) st.st_size))
    {
      GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                                "fstat-size",
                                fn);
      GNUNET_break (0 == close (fd));
      GNUNET_free (fn);
      return;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Loading legal information from file `%s'\n",
                fn);
    {
      void *buf;
      size_t bsize;

      bsize = (size_t) st.st_size;
      buf = mmap (NULL,
                  bsize,
                  PROT_READ,
                  MAP_SHARED,
                  fd,
                  0);
      if (MAP_FAILED == buf)
      {
        GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                                  "mmap",
                                  fn);
        GNUNET_break (0 == close (fd));
        GNUNET_free (fn);
        return;
      }
      GNUNET_break (0 == close (fd));
      GNUNET_free (fn);

      /* insert into global list of terms of service */
      {
        struct Terms *t;
        struct GNUNET_HashCode hc;

        GNUNET_CRYPTO_hash (buf,
                            bsize,
                            &hc);
        t = GNUNET_new (struct Terms);
        t->mime_type = mime;
        t->terms = buf;
        t->language = GNUNET_strdup (lang);
        t->terms_size = bsize;
        t->priority = priority;
        t->terms_etag
          = GNUNET_STRINGS_data_to_string_alloc (&hc,
                                                 sizeof (hc) / 2);
        buf = GNUNET_memdup (t->terms,
                             t->terms_size);
        if (TALER_MHD_body_compress (&buf,
                                     &bsize))
        {
          t->compressed_terms = buf;
          t->compressed_terms_size = bsize;
        }
        else
        {
          GNUNET_free (buf);
        }
        {
          struct Terms *prev = NULL;

          for (struct Terms *pos = legal->terms_head;
               NULL != pos;
               pos = pos->next)
          {
            if (pos->priority < priority)
              break;
            prev = pos;
          }
          GNUNET_CONTAINER_DLL_insert_after (legal->terms_head,
                                             legal->terms_tail,
                                             prev,
                                             t);
        }
      }
    }
  }
}


/**
 * Load all the terms of service from @a path under language @a lang.
 *
 * @param[in,out] legal where to write the result
 * @param path where the terms are found
 * @param lang which language directory to crawl
 */
static void
load_language (struct TALER_MHD_Legal *legal,
               const char *path,
               const char *lang)
{
  char *dname;
  DIR *d;

  GNUNET_asprintf (&dname,
                   "%s/%s",
                   path,
                   lang);
  d = opendir (dname);
  if (NULL == d)
  {
    GNUNET_free (dname);
    return;
  }
  for (struct dirent *de = readdir (d);
       NULL != de;
       de = readdir (d))
  {
    const char *fn = de->d_name;

    if (fn[0] == '.')
      continue;
    load_terms (legal,
                path,
                lang,
                fn);
  }
  GNUNET_break (0 == closedir (d));
  GNUNET_free (dname);
}


struct TALER_MHD_Legal *
TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg,
                      const char *section,
                      const char *diroption,
                      const char *tagoption)
{
  struct TALER_MHD_Legal *legal;
  char *path;
  DIR *d;

  legal = GNUNET_new (struct TALER_MHD_Legal);
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             section,
                                             tagoption,
                                             &legal->terms_version))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               section,
                               tagoption);
    GNUNET_free (legal);
    return NULL;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_filename (cfg,
                                               section,
                                               diroption,
                                               &path))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               section,
                               diroption);
    GNUNET_free (legal->terms_version);
    GNUNET_free (legal);
    return NULL;
  }
  d = opendir (path);
  if (NULL == d)
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
                               section,
                               diroption,
                               "Could not open directory");
    GNUNET_free (legal->terms_version);
    GNUNET_free (legal);
    GNUNET_free (path);
    return NULL;
  }
  for (struct dirent *de = readdir (d);
       NULL != de;
       de = readdir (d))
  {
    const char *lang = de->d_name;

    if (lang[0] == '.')
      continue;
    if (0 == strcmp (lang,
                     "locale"))
      continue;
    load_language (legal,
                   path,
                   lang);
  }
  GNUNET_break (0 == closedir (d));
  GNUNET_free (path);
  return legal;
}


void
TALER_MHD_legal_free (struct TALER_MHD_Legal *legal)
{
  struct Terms *t;
  if (NULL == legal)
    return;
  while (NULL != (t = legal->terms_head))
  {
    GNUNET_free (t->language);
    GNUNET_free (t->compressed_terms);
    if (0 != munmap (t->terms, t->terms_size))
      GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
                           "munmap");
    GNUNET_CONTAINER_DLL_remove (legal->terms_head,
                                 legal->terms_tail,
                                 t);
    GNUNET_free (t->terms_etag);
    GNUNET_free (t);
  }
  GNUNET_free (legal->terms_version);
  GNUNET_free (legal);
}
