/*
     This file is part of GNUnet.
     Copyright (C) 2009, 2010, 2012, 2013, 2016 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
 */

/**
 * @file arm/arm_monitor_api.c
 * @brief API for monitoring the ARM service
 * @author Christian Grothoff
 * @author LRN
 */
#include "platform.h"
#include "gnunet_arm_service.h"
#include "gnunet_util_lib.h"
#include "gnunet_protocols.h"
#include "arm.h"

#define INIT_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 5)

#define LOG(kind, ...) GNUNET_log_from (kind, "arm-monitor-api", __VA_ARGS__)

/**
 * Handle for interacting with ARM.
 */
struct GNUNET_ARM_MonitorHandle
{
  /**
   * Our control connection to the ARM service.
   */
  struct GNUNET_MQ_Handle *mq;

  /**
   * The configuration that we are using.
   */
  const struct GNUNET_CONFIGURATION_Handle *cfg;

  /**
   * ID of the reconnect task (if any).
   */
  struct GNUNET_SCHEDULER_Task *reconnect_task;

  /**
   * Current delay we use for re-trying to connect to core.
   */
  struct GNUNET_TIME_Relative retry_backoff;

  /**
   * Callback to invoke on status updates.
   */
  GNUNET_ARM_ServiceMonitorCallback service_status;

  /**
   * Closure for @e service_status.
   */
  void *service_status_cls;
};


/**
 * Connect to the ARM service for monitoring.
 *
 * @param h handle to connect
 * @return #GNUNET_OK on success
 */
static int
reconnect_arm_monitor (struct GNUNET_ARM_MonitorHandle *h);


/**
 * Task scheduled to try to re-connect to arm.
 *
 * @param cls the `struct GNUNET_ARM_MonitorHandle`
 */
static void
reconnect_arm_monitor_task (void *cls)
{
  struct GNUNET_ARM_MonitorHandle *h = cls;

  h->reconnect_task = NULL;
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Connecting to ARM service for monitoring after delay\n");
  GNUNET_break (GNUNET_OK == reconnect_arm_monitor (h));
}


/**
 * Close down any existing connection to the ARM service and
 * try re-establishing it later.
 *
 * @param h our handle
 */
static void
reconnect_arm_monitor_later (struct GNUNET_ARM_MonitorHandle *h)
{
  if (NULL != h->mq)
  {
    GNUNET_MQ_destroy (h->mq);
    h->mq = NULL;
  }
  GNUNET_assert (NULL == h->reconnect_task);
  h->reconnect_task = GNUNET_SCHEDULER_add_delayed (h->retry_backoff,
                                                    &reconnect_arm_monitor_task,
                                                    h);
  h->retry_backoff = GNUNET_TIME_STD_BACKOFF (h->retry_backoff);
}


/**
 * Check notification messages received from ARM is well-formed.
 *
 * @param cls our `struct GNUNET_ARM_MonitorHandle`
 * @param msg the message received from the arm service
 * @return #GNUNET_OK if the message is well-formed
 */
static int
check_monitor_notify (void *cls, const struct GNUNET_ARM_StatusMessage *msg)
{
  size_t sl =
    ntohs (msg->header.size) - sizeof(struct GNUNET_ARM_StatusMessage);
  const char *name = (const char *) &msg[1];

  (void) cls;
  if ((0 == sl) || ('\0' != name[sl - 1]))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Handler for notification messages received from ARM.
 *
 * @param cls our `struct GNUNET_ARM_MonitorHandle`
 * @param res the message received from the arm service
 */
static void
handle_monitor_notify (void *cls, const struct GNUNET_ARM_StatusMessage *res)
{
  struct GNUNET_ARM_MonitorHandle *h = cls;
  enum GNUNET_ARM_ServiceMonitorStatus status;

  status = ntohl (res->status);
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Received notification from ARM for service `%s' with status %d\n",
       (const char *) &res[1],
       (int) status);
  if (NULL != h->service_status)
    h->service_status (h->service_status_cls, (const char *) &res[1], status);
}


/**
 * Generic error handler, called with the appropriate error code and
 * the same closure specified at the creation of the message queue.
 * Not every message queue implementation supports an error handler.
 *
 * @param cls closure with the `struct GNUNET_ARM_MonitorHandle *`
 * @param error error code
 */
static void
mq_error_handler (void *cls, enum GNUNET_MQ_Error error)
{
  struct GNUNET_ARM_MonitorHandle *h = cls;

  (void) error;
  reconnect_arm_monitor_later (h);
}


/**
 * Connect to the ARM service for monitoring.
 *
 * @param h handle to connect
 * @return #GNUNET_OK on success
 */
static int
reconnect_arm_monitor (struct GNUNET_ARM_MonitorHandle *h)
{
  struct GNUNET_MQ_MessageHandler handlers[] =
  { GNUNET_MQ_hd_var_size (monitor_notify,
                           GNUNET_MESSAGE_TYPE_ARM_STATUS,
                           struct GNUNET_ARM_StatusMessage,
                           h),
    GNUNET_MQ_handler_end () };
  struct GNUNET_MessageHeader *msg;
  struct GNUNET_MQ_Envelope *env;

  GNUNET_assert (NULL == h->mq);
  h->mq = GNUNET_CLIENT_connect (h->cfg, "arm", handlers, &mq_error_handler, h);
  if (NULL == h->mq)
  {
    if (NULL != h->service_status)
      h->service_status (h->service_status_cls,
                         NULL,
                         GNUNET_ARM_SERVICE_STOPPED);
    return GNUNET_SYSERR;
  }
  env = GNUNET_MQ_msg (msg, GNUNET_MESSAGE_TYPE_ARM_MONITOR);
  GNUNET_MQ_send (h->mq, env);
  return GNUNET_OK;
}


/**
 * Setup a context for monitoring ARM, then
 * start connecting to the ARM service for monitoring using that context.
 *
 * @param cfg configuration to use (needed to contact ARM;
 *        the ARM service may internally use a different
 *        configuration to determine how to start the service).
 * @param cont callback to invoke on status updates
 * @param cont_cls closure for @a cont
 * @return context to use for further ARM monitor operations, NULL on error.
 */
struct GNUNET_ARM_MonitorHandle *
GNUNET_ARM_monitor_start (const struct GNUNET_CONFIGURATION_Handle *cfg,
                          GNUNET_ARM_ServiceMonitorCallback cont,
                          void *cont_cls)
{
  struct GNUNET_ARM_MonitorHandle *h;

  h = GNUNET_new (struct GNUNET_ARM_MonitorHandle);
  h->cfg = cfg;
  h->service_status = cont;
  h->service_status_cls = cont_cls;
  if (GNUNET_OK != reconnect_arm_monitor (h))
  {
    GNUNET_free (h);
    return NULL;
  }
  return h;
}


/**
 * Disconnect from the ARM service (if connected) and destroy the context.
 *
 * @param h the handle that was being used
 */
void
GNUNET_ARM_monitor_stop (struct GNUNET_ARM_MonitorHandle *h)
{
  if (NULL != h->mq)
  {
    GNUNET_MQ_destroy (h->mq);
    h->mq = NULL;
  }
  if (NULL != h->reconnect_task)
  {
    GNUNET_SCHEDULER_cancel (h->reconnect_task);
    h->reconnect_task = NULL;
  }
  GNUNET_free (h);
}


/* end of arm_api.c */
