/*!
 * @file File: mam.c
 *
 * vim: expandtab:ts=2:sts=2:sw=2
 *
 * @copyright 
 * 
 * Copyright (C) 2020 Anoxinon e.V. 
 *
 * This file is part of xmppc.
 *
 * xmppc 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 of the License, or
 * (at your option) any later version.
 *
 * xmppc 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 Foobar.  If not, see <http://www.gnu.org/licenses/>.
 *
 * German
 *
 * Diese Datei ist Teil von xmppc.
 *
 * xmppc ist Freie Software: Sie können es unter den Bedingungen
 * der GNU General Public License, wie von der Free Software Foundation,
 * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder neueren
 * veröffentlichten Version, weiter verteilen und/oder modifizieren.
 *
 * xmppc wird in der Hoffnung, dass es nützlich sein wird, aber
 * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
 * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
 * Siehe die GNU General Public License für weitere Details.
 *
 * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
 * Programm erhalten haben. Wenn nicht, siehe <https://www.gnu.org/licenses/>.
 */

#include "mam.h"
#include "glib.h"
#include "stdbool.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"

#define MAM_NS "urn:xmpp:mam:2"
#define FORWARD_NS "urn:xmpp:forward:0"
#define DELAY_NS "urn:xmpp:delay"
#define RSM_NS "http://jabber.org/protocol/rsm"

#define MAM_DEFAULT_MAX 5
#define MAM_MAX_CAP 50
#define MAM_DEFAULT_TIMEOUT_SECONDS 300
#define MAM_POLL_PERIOD_MS 2000

typedef struct {
  char *with;
  char *start;
  char *end;
  char *after_id;
  char *before_id;
  char *ids;
  int max;
  int timeout;
  bool has_narrowing;
} mam_options_t;

typedef struct {
  xmppc_t *xmppc;
  mam_options_t options;
  bool receive;
  int messages_total;
  gint64 deadline_us;
  char *queryid;
} mam_run_t;

static void _mam_options_init(mam_options_t *options);
static void _mam_options_free(mam_options_t *options);
static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive);
static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive);
static void _mam_send_request(mam_run_t *run);
static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata);
static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run);
static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids);
static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty);
static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value);
static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value);
static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds);
static char *_mam_normalize_datetime(const char *value);
static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result);
static void _mam_print_compact_message(xmpp_stanza_t *result);
static void _mam_print_escaped_attr(const char *text);
static void _mam_print_escaped_text(const char *text);
static void _mam_print_escaped(const char *text, bool attribute);
static void _mam_usage(xmppc_t *xmppc);

void mam_execute_command (xmppc_t *xmppc, int argc, char *argv[]) {
  mam_options_t options;

  if(argc >= 1) {
    if (strcmp("list", argv[0]) == 0) {
      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
        _mam_start_request(xmppc, &options, false, false);
      } else {
        xmpp_disconnect(xmppc->conn);
      }
      return;
    } else if (strcmp("pretty", argv[0]) == 0) {
      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
        _mam_start_request(xmppc, &options, true, false);
      } else {
        xmpp_disconnect(xmppc->conn);
      }
      return;
    } else if (strcmp("receive", argv[0]) == 0) {
      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, true)) {
        _mam_start_request(xmppc, &options, false, true);
      } else {
        xmpp_disconnect(xmppc->conn);
      }
      return;
    }
  }

  _mam_usage(xmppc);
  xmpp_disconnect(xmppc->conn);
}

static void _mam_options_init(mam_options_t *options) {
  memset(options, 0, sizeof(*options));
  options->max = MAM_DEFAULT_MAX;
  options->timeout = MAM_DEFAULT_TIMEOUT_SECONDS;
}

static void _mam_options_free(mam_options_t *options) {
  g_free(options->with);
  g_free(options->start);
  g_free(options->end);
  g_free(options->after_id);
  g_free(options->before_id);
  g_free(options->ids);
}

static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive) {
  _mam_options_init(options);

  for (int i = 0; i < argc; i++) {
    char *term = argv[i];
    char *equals = strchr(term, '=');

    if (!equals) {
      if (!_mam_set_string_option(xmppc, &options->with, "with", term, false)) {
        _mam_options_free(options);
        return false;
      }
      options->has_narrowing = true;
      continue;
    }

    char *key = g_strndup(term, equals - term);
    const char *value = equals + 1;

    if (strcmp(key, "with") == 0) {
      if (!_mam_set_string_option(xmppc, &options->with, key, value, false)) {
        g_free(key);
        _mam_options_free(options);
        return false;
      }
      options->has_narrowing = true;
    } else if (strcmp(key, "start") == 0) {
      char *normalized = _mam_normalize_datetime(value);
      if (!_mam_set_string_option(xmppc, &options->start, key, normalized, false)) {
        g_free(normalized);
        g_free(key);
        _mam_options_free(options);
        return false;
      }
      g_free(normalized);
      options->has_narrowing = true;
    } else if (strcmp(key, "end") == 0) {
      char *normalized = _mam_normalize_datetime(value);
      if (!_mam_set_string_option(xmppc, &options->end, key, normalized, false)) {
        g_free(normalized);
        g_free(key);
        _mam_options_free(options);
        return false;
      }
      g_free(normalized);
      options->has_narrowing = true;
    } else if (strcmp(key, "after-id") == 0) {
      if (!_mam_set_string_option(xmppc, &options->after_id, key, value, false)) {
        g_free(key);
        _mam_options_free(options);
        return false;
      }
      options->has_narrowing = true;
    } else if (strcmp(key, "before-id") == 0) {
      if (!_mam_set_string_option(xmppc, &options->before_id, key, value, true)) {
        g_free(key);
        _mam_options_free(options);
        return false;
      }
      options->has_narrowing = true;
    } else if (strcmp(key, "ids") == 0) {
      if (!_mam_set_string_option(xmppc, &options->ids, key, value, false)) {
        g_free(key);
        _mam_options_free(options);
        return false;
      }
      options->has_narrowing = true;
    } else if (strcmp(key, "max") == 0) {
      if (!_mam_parse_max(xmppc, options, value)) {
        g_free(key);
        _mam_options_free(options);
        return false;
      }
    } else if (receive && strcmp(key, "timeout") == 0) {
      if (!_mam_parse_timeout(xmppc, options, value)) {
        g_free(key);
        _mam_options_free(options);
        return false;
      }
    } else {
      logError(xmppc, "Unknown MAM field: %s\n", key);
      g_free(key);
      _mam_options_free(options);
      return false;
    }

    g_free(key);
  }

  if (receive) {
    if (!options->with) {
      logError(xmppc, "mam receive requires a JID or with=<jid>\n");
      _mam_options_free(options);
      return false;
    }
    if (!options->after_id) {
      logError(xmppc, "mam receive requires after-id=<mam-id>\n");
      _mam_options_free(options);
      return false;
    }
  } else if (!options->has_narrowing) {
    logError(xmppc, "mam list requires a JID, with=, start=, end=, after-id=, before-id=, or ids=\n");
    _mam_options_free(options);
    return false;
  }

  if (!options->start && !options->end && !options->after_id && !options->before_id && !options->ids) {
    char *start = _mam_normalize_datetime("-5m");
    options->start = start;
  }

  return true;
}

static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive) {
  mam_run_t *run = g_new0(mam_run_t, 1);
  run->xmppc = xmppc;
  run->options = *options;
  run->receive = receive;
  if (receive) {
    run->deadline_us = g_get_monotonic_time() + ((gint64)run->options.timeout * G_USEC_PER_SEC);
  }

  if (pretty) {
    xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, run);
  } else {
    xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, run);
  }

  _mam_send_request(run);
}

static void _mam_send_request(mam_run_t *run) {
  xmppc_t *xmppc = run->xmppc;
  xmpp_ctx_t *ctx = xmppc->ctx;
  char max[16];

  g_snprintf(max, sizeof(max), "%d", run->options.max);

  char* id = xmpp_uuid_gen(ctx);
  g_free(run->queryid);
  run->queryid = g_strdup(id);

  xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
  xmpp_stanza_t *query = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(query, "query");
  xmpp_stanza_set_ns(query, MAM_NS);
  xmpp_stanza_set_attribute(query, "queryid", id);

  xmpp_stanza_t* x = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(x,"x");
  xmpp_stanza_set_ns(x, "jabber:x:data");
  xmpp_stanza_set_type(x,"submit");
  xmpp_stanza_add_child(query,x);

  _mam_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");

  if (run->options.with) {
    _mam_add_field(ctx, x, "with", run->options.with, NULL);
  }
  if (run->options.start) {
    _mam_add_field(ctx, x, "start", run->options.start, NULL);
  }
  if (run->options.end) {
    _mam_add_field(ctx, x, "end", run->options.end, NULL);
  }
  if (run->options.ids) {
    _mam_add_ids_field(ctx, x, run->options.ids);
  }

  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(set, "set");
  xmpp_stanza_set_ns(set, RSM_NS);
  _mam_add_text_child(ctx, set, "max", max);
  if (run->options.after_id) {
    _mam_add_text_child(ctx, set, "after", run->options.after_id);
  }
  if (run->options.before_id) {
    _mam_add_text_child(ctx, set, "before", run->options.before_id);
  }
  xmpp_stanza_add_child(query, set);

  xmpp_stanza_add_child(iq,query);
  xmpp_id_handler_add(xmppc->conn, _mam_finish, id, run);
  xmpp_send(xmppc->conn,iq);

  xmpp_stanza_release(iq);
  xmpp_stanza_release(query);
  xmpp_stanza_release(x);
  xmpp_stanza_release(set);
  free(id);
}

static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
  mam_run_t *run = (mam_run_t *)userdata;
  const char *type = xmpp_stanza_get_type(stanza);

  if (type && strcmp(type, "error") == 0) {
    logError(run->xmppc, "MAM request failed\n");
    _mam_disconnect_and_free(conn, run);
    return 0;
  }

  xmpp_stanza_t *fin = xmpp_stanza_get_child_by_name(stanza,"fin");
  if(fin) {
    if (run->receive && run->messages_total == 0 && g_get_monotonic_time() < run->deadline_us) {
      xmpp_timed_handler_add(conn, _mam_receive_poll, MAM_POLL_PERIOD_MS, run);
    } else {
      _mam_disconnect_and_free(conn, run);
    }
  }
  return 0;
}

static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata) {
  mam_run_t *run = (mam_run_t *)userdata;

  if (g_get_monotonic_time() >= run->deadline_us) {
    _mam_disconnect_and_free(conn, run);
    return 0;
  }

  _mam_send_request(run);
  return 0;
}

static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run) {
  xmpp_handler_delete(conn, _mam_display_message);
  xmpp_handler_delete(conn, _mam_display_pretty_message);
  _mam_options_free(&run->options);
  g_free(run->queryid);
  g_free(run);
  xmpp_disconnect(conn);
}

static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(field,"field");
  xmpp_stanza_set_attribute(field, "var", var);
  if (type) {
    xmpp_stanza_set_type(field, type);
  }

  _mam_add_text_child(ctx, field, "value", value);
  xmpp_stanza_add_child(x, field);
  xmpp_stanza_release(field);
}

static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids) {
  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(field,"field");
  xmpp_stanza_set_attribute(field, "var", "ids");

  char **parts = g_strsplit(ids, ",", -1);
  for (int i = 0; parts[i] != NULL; i++) {
    if (parts[i][0] != '\0') {
      _mam_add_text_child(ctx, field, "value", parts[i]);
    }
  }
  g_strfreev(parts);

  xmpp_stanza_add_child(x, field);
  xmpp_stanza_release(field);
}

static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
  xmpp_stanza_t *child = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(child, name);

  xmpp_stanza_t *body = xmpp_stanza_new(ctx);
  xmpp_stanza_set_text(body, text ? text : "");
  xmpp_stanza_add_child(child, body);
  xmpp_stanza_release(body);

  xmpp_stanza_add_child(parent, child);
  xmpp_stanza_release(child);
}

static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty) {
  if (*target) {
    logError(xmppc, "Duplicate MAM field: %s\n", key);
    return false;
  }
  if (!allow_empty && (!value || value[0] == '\0')) {
    logError(xmppc, "MAM field requires a value: %s\n", key);
    return false;
  }
  *target = g_strdup(value ? value : "");
  return true;
}

static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value) {
  char *end = NULL;
  gint64 parsed = g_ascii_strtoll(value, &end, 10);

  if (!value || value[0] == '\0' || !end || *end != '\0' || parsed < 1) {
    logError(xmppc, "max must be a positive integer\n");
    return false;
  }

  if (parsed > MAM_MAX_CAP) {
    parsed = MAM_MAX_CAP;
  }

  options->max = (int)parsed;
  return true;
}

static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value) {
  gint64 seconds = 0;

  if (!_mam_parse_duration_seconds(value, &seconds) || seconds < 0 || seconds > G_MAXINT) {
    logError(xmppc, "timeout must be a duration like 30s, 5m, or 1h\n");
    return false;
  }

  options->timeout = (int)seconds;
  return true;
}

static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds) {
  char *end = NULL;
  gint64 parsed = 0;
  gint64 multiplier = 1;

  if (!value || value[0] == '\0') {
    return false;
  }

  parsed = g_ascii_strtoll(value, &end, 10);
  if (end == value || parsed < 0) {
    return false;
  }

  if (*end == '\0') {
    multiplier = 1;
  } else if (end[1] == '\0') {
    if (*end == 's') {
      multiplier = 1;
    } else if (*end == 'm') {
      multiplier = 60;
    } else if (*end == 'h') {
      multiplier = 60 * 60;
    } else if (*end == 'd') {
      multiplier = 60 * 60 * 24;
    } else {
      return false;
    }
  } else {
    return false;
  }

  *seconds = parsed * multiplier;
  return true;
}

static char *_mam_normalize_datetime(const char *value) {
  if (value && value[0] == '-' && value[1] != '\0') {
    gint64 seconds = 0;
    if (_mam_parse_duration_seconds(value + 1, &seconds) && seconds > 0) {
      GDateTime *now = g_date_time_new_now_utc();
      GDateTime *then = g_date_time_add_seconds(now, -seconds);
      char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
      g_date_time_unref(then);
      g_date_time_unref(now);
      return formatted;
    }
  }

  return g_strdup(value);
}

static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result) {
  const char *ns = xmpp_stanza_get_ns(result);
  if (!ns || strcmp(MAM_NS, ns) != 0) {
    return false;
  }

  const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
  return !queryid || !run->queryid || strcmp(queryid, run->queryid) == 0;
}

static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
  mam_run_t *run = (mam_run_t *)userdata;
  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");

  if (result && _mam_result_matches(run, result) && run->messages_total < run->options.max) {
    _mam_print_compact_message(result);
    run->messages_total++;
  }
  return 1;
}

static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
{
  mam_run_t *run = (mam_run_t *)userdata;
  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");

  if( result && _mam_result_matches(run, result) && run->messages_total < run->options.max ) {

    xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
    if (forwarded) {

      xmpp_stanza_t* message = xmpp_stanza_get_child_by_name(forwarded, "message");
      if (message) {
        const char* const from = xmpp_stanza_get_from(message);
        xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");

        if (body) {
          char *text = xmpp_stanza_get_text(body);
          printf("%s: %s\n", from ? from : "", text ? text : "");
          free(text);
          run->messages_total++;
        }
      }
    }
  }

  return 1;
}

static void _mam_print_compact_message(xmpp_stanza_t *result) {
  const char *mam_id = xmpp_stanza_get_attribute(result, "id");
  const char *message_id = "";
  const char *stamp = "";
  const char *from = "";
  const char *to = "";
  char *body_text = NULL;

  xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
  xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;

  if (forwarded) {
    xmpp_stanza_t* delay = xmpp_stanza_get_child_by_ns(forwarded, DELAY_NS);
    if (delay && xmpp_stanza_get_attribute(delay, "stamp")) {
      stamp = xmpp_stanza_get_attribute(delay, "stamp");
    }
  }

  if (message) {
    xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
    if (xmpp_stanza_get_id(message)) {
      message_id = xmpp_stanza_get_id(message);
    }
    if (xmpp_stanza_get_from(message)) {
      from = xmpp_stanza_get_from(message);
    }
    if (xmpp_stanza_get_to(message)) {
      to = xmpp_stanza_get_to(message);
    }
    if (body) {
      body_text = xmpp_stanza_get_text(body);
    }
  }

  printf("<message id=\"");
  _mam_print_escaped_attr(mam_id ? mam_id : "");
  printf("\" message-id=\"");
  _mam_print_escaped_attr(message_id);
  printf("\" stamp=\"");
  _mam_print_escaped_attr(stamp);
  printf("\" from=\"");
  _mam_print_escaped_attr(from);
  printf("\" to=\"");
  _mam_print_escaped_attr(to);
  printf("\"><body>");
  _mam_print_escaped_text(body_text ? body_text : "");
  printf("</body></message>\n");
  fflush(stdout);

  free(body_text);
}

static void _mam_print_escaped_attr(const char *text) {
  _mam_print_escaped(text, true);
}

static void _mam_print_escaped_text(const char *text) {
  _mam_print_escaped(text, false);
}

static void _mam_print_escaped(const char *text, bool attribute) {
  const unsigned char *p = (const unsigned char *)(text ? text : "");

  while (*p) {
    switch (*p) {
      case '&':
        printf("&amp;");
        break;
      case '<':
        printf("&lt;");
        break;
      case '>':
        printf("&gt;");
        break;
      case '"':
        if (attribute) {
          printf("&quot;");
        } else {
          putchar('"');
        }
        break;
      case '\n':
        printf("&#10;");
        break;
      case '\r':
        printf("&#13;");
        break;
      case '\t':
        if (attribute) {
          printf("&#9;");
        } else {
          putchar('\t');
        }
        break;
      default:
        if (*p < 0x20) {
          printf("&#x%02X;", *p);
        } else {
          putchar(*p);
        }
        break;
    }
    p++;
  }
}

static void _mam_usage(xmppc_t *xmppc) {
  logError(xmppc, "Usage:\n");
  logError(xmppc, "  mam list <jid> [with=<jid>] [start=<time>] [end=<time>] [after-id=<id>] [before-id=<id>] [ids=<id>[,<id>...]] [max=<n>]\n");
  logError(xmppc, "  mam receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
}
