/*
  This file is part of TALER
  Copyright (C) 2025 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 mhd_typst.c
 * @brief MHD utility functions for PDF generation
 * @author Christian Grothoff
 *
 *
 */
#include "taler/platform.h"
#include "taler/taler_util.h"
#include "taler/taler_mhd_lib.h"
#include <microhttpd.h>


/**
 * Information about a specific typst invocation.
 */
struct TypstStage
{
  /**
   * Name of the FIFO for the typst output.
   */
  char *filename;

  /**
   * Typst context we are part of.
   */
  struct TALER_MHD_TypstContext *tc;

  /**
   * Handle to the typst process.
   */
  struct GNUNET_OS_Process *proc;

  /**
   * Handle to be notified about stage completion.
   */
  struct GNUNET_ChildWaitHandle *cwh;

};


struct TALER_MHD_TypstContext
{

  /**
   * Directory where we create temporary files (or FIFOs) for the IPC.
   */
  char *tmpdir;

  /**
   * Array of stages producing PDFs to be combined.
   */
  struct TypstStage *stages;

  /**
   * Handle for pdftk combining the various PDFs.
   */
  struct GNUNET_OS_Process *proc;

  /**
   * Handle to wait for @e proc to complete.
   */
  struct GNUNET_ChildWaitHandle *cwh;

  /**
   * Callback to call on the final result.
   */
  TALER_MHD_TypstResultCallback cb;

  /**
   * Closure for @e cb
   */
  void *cb_cls;

  /**
   * Task for async work.
   */
  struct GNUNET_SCHEDULER_Task *t;

  /**
   * Name of the final file created by pdftk.
   */
  char *output_file;

  /**
   * Length of the @e stages array.
   */
  unsigned int num_stages;

  /**
   * Number of still active stages.
   */
  unsigned int active_stages;

  /**
   * Should the directory be removed when done?
   */
  bool remove_on_exit;
};


void
TALER_MHD_typst_cancel (struct TALER_MHD_TypstContext *tc)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Cleaning up TypstContext\n");
  if (NULL != tc->t)
  {
    GNUNET_SCHEDULER_cancel (tc->t);
    tc->t = NULL;
  }
  for (unsigned int i = 0; i<tc->num_stages; i++)
  {
    struct TypstStage *stage = &tc->stages[i];

    if (NULL != stage->cwh)
    {
      GNUNET_wait_child_cancel (stage->cwh);
      stage->cwh = NULL;
    }
    if (NULL != stage->proc)
    {
      GNUNET_break (0 ==
                    GNUNET_OS_process_kill (stage->proc,
                                            SIGKILL));
      GNUNET_OS_process_destroy (stage->proc);
      stage->proc = NULL;
    }
    GNUNET_free (stage->filename);
  }
  GNUNET_free (tc->stages);
  if (NULL != tc->cwh)
  {
    GNUNET_wait_child_cancel (tc->cwh);
    tc->cwh = NULL;
  }
  if (NULL != tc->proc)
  {
    GNUNET_break (0 ==
                  GNUNET_OS_process_kill (tc->proc,
                                          SIGKILL));
    GNUNET_OS_process_destroy (tc->proc);
  }
  GNUNET_free (tc->output_file);
  if (NULL != tc->tmpdir)
  {
    if (tc->remove_on_exit)
      GNUNET_DISK_directory_remove (tc->tmpdir);
    GNUNET_free (tc->tmpdir);
  }
  GNUNET_free (tc);
}


/**
 * Create file in @a tmpdir with one of the PDF inputs.
 *
 * @param[out] stage initialized stage data
 * @param tmpdir where to place temporary files
 * @param data input JSON with PDF data
 * @return true on success
 */
static bool
inline_pdf_stage (struct TypstStage *stage,
                  const char *tmpdir,
                  const json_t *data)
{
  const char *str = json_string_value (data);
  char *fn;
  size_t n;
  void *b;

  if (NULL == str)
  {
    GNUNET_break (0);
    return false;
  }
  if (GNUNET_OK !=
      GNUNET_STRINGS_string_to_data_alloc (str,
                                           strlen (str),
                                           &b,
                                           &n))
  {
    GNUNET_break (0);
    return false;
  }
  GNUNET_asprintf (&fn,
                   "%s/external-XXXXXX",
                   tmpdir);
  stage->filename = GNUNET_DISK_mktemp (fn);
  if (NULL == stage->filename)
  {
    GNUNET_break (0);
    GNUNET_free (b);
    GNUNET_free (fn);
    return false;
  }
  GNUNET_free (fn);

  if (n !=
      GNUNET_DISK_fn_write (stage->filename,
                            b,
                            n,
                            GNUNET_DISK_PERM_USER_READ
                            | GNUNET_DISK_PERM_USER_WRITE))
  {
    GNUNET_free (b);
    GNUNET_free (stage->filename);
    return false;
  }
  return true;
}


/**
 * Generate a response for @a tc indicating an error of type @a ec.
 *
 * @param[in,out] tc context to fail
 * @param ec error code to return
 * @param hint hint text to return
 */
static void
typst_context_fail (struct TALER_MHD_TypstContext *tc,
                    enum TALER_ErrorCode ec,
                    const char *hint)
{
  struct TALER_MHD_TypstResponse resp = {
    .ec = ec,
    .details.hint = hint
  };

  if (NULL != tc->cb)
  {
    tc->cb (tc->cb_cls,
            &resp);
    tc->cb = NULL;
  }
}


/**
 * Called when the pdftk helper exited.
 *
 * @param cls our `struct TALER_MHD_TypstContext *`
 * @param type type of the process
 * @param exit_code status code of the process
 */
static void
pdftk_done_cb (void *cls,
               enum GNUNET_OS_ProcessStatusType type,
               long unsigned int exit_code)
{
  struct TALER_MHD_TypstContext *tc = cls;

  tc->cwh = NULL;
  GNUNET_OS_process_destroy (tc->proc);
  tc->proc = NULL;
  switch (type)
  {
  case GNUNET_OS_PROCESS_UNKNOWN:
    GNUNET_assert (0);
    return;
  case GNUNET_OS_PROCESS_RUNNING:
    /* we should not get this notification */
    GNUNET_break (0);
    return;
  case GNUNET_OS_PROCESS_STOPPED:
    /* Someone is SIGSTOPing our helper!? */
    GNUNET_break (0);
    return;
  case GNUNET_OS_PROCESS_EXITED:
    if (0 != exit_code)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "pdftk exited with status %d\n",
                  (int) exit_code);
      typst_context_fail (tc,
                          42,
                          "pdftk failed");
    }
    else
    {
      struct TALER_MHD_TypstResponse resp = {
        .ec = TALER_EC_NONE,
        .details.filename = tc->output_file,
      };

      GNUNET_assert (NULL != tc->cb);
      tc->cb (tc->cb_cls,
              &resp);
      tc->cb = NULL;
    }
    break;
  case GNUNET_OS_PROCESS_SIGNALED:
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "pdftk died with signal %d\n",
                (int) exit_code);
    typst_context_fail (tc,
                        42,
                        "pdftk killed by signal");
    break;
  }
  TALER_MHD_typst_cancel (tc);
}


/**
 * Function called once all of the individual stages are done.
 * Triggers the pdftk run for @a tc.
 *
 * @param[in,out] cls a `struct TALER_MHD_TypstContext *` context to run pdftk for
 */
static void
complete_response (void *cls)
{
  struct TALER_MHD_TypstContext *tc = cls;
  const char *argv[tc->num_stages + 5];

  tc->t = NULL;
  argv[0] = "pdftk";
  for (unsigned int i = 0; i<tc->num_stages; i++)
    argv[i + 1] = tc->stages[i].filename;
  argv[tc->num_stages + 1] = "cat";
  argv[tc->num_stages + 2] = "output";
  argv[tc->num_stages + 3] = tc->output_file;
  argv[tc->num_stages + 4] = NULL;
  tc->proc = GNUNET_OS_start_process_vap (
    GNUNET_OS_INHERIT_STD_ERR,
    NULL,
    NULL,
    NULL,
    "pdftk",
    (char **) argv);
  if (NULL == tc->proc)
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "fork");
    TALER_MHD_typst_cancel (tc);
    return;
  }
  tc->cwh = GNUNET_wait_child (tc->proc,
                               &pdftk_done_cb,
                               tc);
  GNUNET_assert (NULL != tc->cwh);
}


/**
 * Called when a typst helper exited.
 *
 * @param cls our `struct TypstStage *`
 * @param type type of the process
 * @param exit_code status code of the process
 */
static void
typst_done_cb (void *cls,
               enum GNUNET_OS_ProcessStatusType type,
               long unsigned int exit_code)
{
  struct TypstStage *stage = cls;
  struct TALER_MHD_TypstContext *tc = stage->tc;

  stage->cwh = NULL;
  GNUNET_OS_process_destroy (stage->proc);
  stage->proc = NULL;
  switch (type)
  {
  case GNUNET_OS_PROCESS_UNKNOWN:
    GNUNET_assert (0);
    return;
  case GNUNET_OS_PROCESS_RUNNING:
    /* we should not get this notification */
    GNUNET_break (0);
    return;
  case GNUNET_OS_PROCESS_STOPPED:
    /* Someone is SIGSTOPing our helper!? */
    GNUNET_break (0);
    return;
  case GNUNET_OS_PROCESS_EXITED:
    if (0 != exit_code)
    {
      char err[128];

      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "typst exited with status %d\n",
                  (int) exit_code);
      GNUNET_snprintf (err,
                       sizeof (err),
                       "Typst exited with status %d",
                       (int) exit_code);
      typst_context_fail (tc,
                          42,
                          err);
      TALER_MHD_typst_cancel (tc);
      return;
    }
    break;
  case GNUNET_OS_PROCESS_SIGNALED:
    {
      char err[128];

      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "typst died with signal %d\n",
                  (int) exit_code);
      GNUNET_snprintf (err,
                       sizeof (err),
                       "Typst died with signal %d",
                       (int) exit_code);
      typst_context_fail (tc,
                          42,
                          err);
      TALER_MHD_typst_cancel (tc);
      return;
    }
    break;
  }
  tc->active_stages--;
  if (NULL != stage->proc)
  {
    GNUNET_OS_process_destroy (stage->proc);
    stage->proc = NULL;
  }
  if (0 != tc->active_stages)
    return;
  GNUNET_assert (NULL == tc->t);
  tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
                                    tc);
}


/**
 * Setup typst stage to produce one of the PDF inputs.
 *
 * @param[out] stage initialized stage data
 * @param i index of the stage
 * @param tmpdir where to place temporary files
 * @param template_path where to find templates
 * @param doc input document specification
 * @return true on success
 */
static bool
setup_stage (struct TypstStage *stage,
             unsigned int i,
             const char *tmpdir,
             const char *template_path,
             const struct TALER_MHD_TypstDocument *doc)
{
  char *input;

  if (NULL == doc->form_name)
  {
    return inline_pdf_stage (stage,
                             tmpdir,
                             doc->data);
  }

  /* Setup inputs */
  {
    char *dirname;

    GNUNET_asprintf (&dirname,
                     "%s/%u/",
                     tmpdir,
                     i);
    if (GNUNET_OK !=
        GNUNET_DISK_directory_create (dirname))
    {
      GNUNET_free (dirname);
      return false;
    }
    GNUNET_free (dirname);
  }

  /* Setup data input */
  {
    char *jfn;

    GNUNET_asprintf (&jfn,
                     "%s/%u/input.json",
                     tmpdir,
                     i);
    if (0 !=
        json_dump_file (doc->data,
                        jfn,
                        JSON_INDENT (2)
                        // JSON_COMPACT
                        ))
    {
      GNUNET_break (0);
      GNUNET_free (jfn);
      return false;
    }
    GNUNET_free (jfn);
  }

  /* setup output file name */
  GNUNET_asprintf (&stage->filename,
                   "%s/%u/input.pdf",
                   tmpdir,
                   i);

  /* setup main input Typst file */
  {
    char *intyp;
    char *template_fn;

    GNUNET_asprintf (&template_fn,
                     "%s%s",
                     template_path,
                     doc->form_name);
    if (GNUNET_YES !=
        GNUNET_DISK_file_test_read (template_fn))
    {
      GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                                "access",
                                template_fn);
      GNUNET_free (template_fn);
      return false;
    }
    GNUNET_asprintf (&intyp,
                     "#import \"%s\": form\n"
                     "#form(json(\"%s/%u/input.json\"))\n",
                     template_fn,
                     tmpdir,
                     i);
    GNUNET_asprintf (&input,
                     "%s/%u/input.typ",
                     tmpdir,
                     i);
    if (GNUNET_OK !=
        GNUNET_DISK_fn_write (input,
                              intyp,
                              strlen (intyp),
                              GNUNET_DISK_PERM_USER_READ))
    {
      GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                                "write",
                                input);
      GNUNET_free (input);
      GNUNET_free (intyp);
      GNUNET_free (template_fn);
      return false;
    }
    GNUNET_free (template_fn);
    GNUNET_free (intyp);
  }

  /* now setup typst invocation */
  {
    const char *argv[6];

    argv[0] = "typst";
    argv[1] = "compile";
    /* This deliberately breaks the typst sandbox. Why? Because
       they suck and do not support multiple roots, but we have
       dynamic data in /tmp and resources outside of /tmp and
       copying all the time is also bad. Typst should really
       support multiple roots. */
    argv[2] = "--root";
    argv[3] = "/";
    argv[4] = input;
    argv[5] = NULL;
    stage->proc = GNUNET_OS_start_process_vap (
      GNUNET_OS_INHERIT_STD_ERR,
      NULL,
      NULL,
      NULL,
      "typst",
      (char **) argv);
    if (NULL == stage->proc)
    {
      GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                           "fork");
      GNUNET_free (input);
      return false;
    }
    GNUNET_free (input);
    stage->tc->active_stages++;
    stage->cwh = GNUNET_wait_child (stage->proc,
                                    &typst_done_cb,
                                    stage);
    GNUNET_assert (NULL != stage->cwh);
  }
  return true;
}


struct TALER_MHD_TypstContext *
TALER_MHD_typst (
  const struct GNUNET_CONFIGURATION_Handle *cfg,
  bool remove_on_exit,
  const char *cfg_section_name,
  unsigned int num_documents,
  const struct TALER_MHD_TypstDocument docs[static num_documents],
  TALER_MHD_TypstResultCallback cb,
  void *cb_cls)
{
  static enum GNUNET_GenericReturnValue once = GNUNET_NO;
  struct TALER_MHD_TypstContext *tc;

  switch (once)
  {
  case GNUNET_OK:
    break;
  case GNUNET_NO:
    if (GNUNET_SYSERR ==
        GNUNET_OS_check_helper_binary ("typst",
                                       false,
                                       NULL))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "`typst' command not found\n");
      once = GNUNET_SYSERR;
      return NULL;
    }
    if (GNUNET_SYSERR ==
        GNUNET_OS_check_helper_binary ("pdftk",
                                       false,
                                       NULL))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "`pdftk' command not found\n");
      once = GNUNET_SYSERR;
      return NULL;
    }
    once = GNUNET_OK;
    break;
  case GNUNET_SYSERR:
    return NULL;
  }
  tc = GNUNET_new (struct TALER_MHD_TypstContext);
  tc->tmpdir = GNUNET_strdup ("/tmp/taler-typst-XXXXXX");
  tc->remove_on_exit = remove_on_exit;
  tc->cb = cb;
  tc->cb_cls = cb_cls;
  if (NULL == mkdtemp (tc->tmpdir))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                              "mkdtemp",
                              tc->tmpdir);
    GNUNET_free (tc->tmpdir);
    TALER_MHD_typst_cancel (tc);
    return NULL;
  }
  GNUNET_asprintf (&tc->output_file,
                   "%s/final.pdf",
                   tc->tmpdir);

  /* setup typst stages */
  {
    char *template_path;

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_filename (cfg,
                                                 cfg_section_name,
                                                 "TYPST_TEMPLATES",
                                                 &template_path))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 cfg_section_name,
                                 "TYPST_TEMPLATES");
      TALER_MHD_typst_cancel (tc);
      return NULL;
    }
    tc->stages = GNUNET_new_array (num_documents,
                                   struct TypstStage);
    tc->num_stages = num_documents;
    for (unsigned int i = 0; i<num_documents; i++)
    {
      tc->stages[i].tc = tc;
      if (! setup_stage (&tc->stages[i],
                         i,
                         tc->tmpdir,
                         template_path,
                         &docs[i]))
      {
        char err[128];

        GNUNET_snprintf (err,
                         sizeof (err),
                         "Typst setup failed on stage %u",
                         i);
        typst_context_fail (tc,
                            42,
                            err);
        TALER_MHD_typst_cancel (tc);
        return NULL;
      }
    }
    GNUNET_free (template_path);
  }
  if (0 == tc->active_stages)
  {
    tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
                                      tc);
  }
  return tc;
}


struct MHD_Response *
TALER_MHD_response_from_pdf_file (const char *filename)
{
  struct MHD_Response *resp;
  struct stat s;
  int fd;

  fd = open (filename,
             O_RDONLY);
  if (-1 == fd)
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                              "open",
                              filename);
    return NULL;
  }
  if (0 !=
      fstat (fd,
             &s))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                              "fstat",
                              filename);
    GNUNET_assert (0 == close (fd));
    return NULL;
  }
  resp = MHD_create_response_from_fd (s.st_size,
                                      fd);
  TALER_MHD_add_global_headers (resp,
                                false);
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (resp,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         "application/pdf"));
  return resp;
}
