/*
     This file is part of GNUnet.
     Copyright (C) 2009-2014, 2016, 2026 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 core/gnunet-service-core_sessions.c
 * @brief code for managing of 'encrypted' sessions (key exchange done)
 * @author Christian Grothoff
 */
#include "gnunet_common.h"
#include "platform.h"
#include "gnunet-service-core.h"
#include "gnunet-service-core_kx.h"
#include "gnunet-service-core_sessions.h"
#include "gnunet_constants.h"
#include "core.h"


/**
 * How many encrypted messages do we queue at most?
 * Needed to bound memory consumption.
 */
#define MAX_ENCRYPTED_MESSAGE_QUEUE_SIZE 4


/**
 * Message ready for encryption.  This struct is followed by the
 * actual content of the message.
 */
struct SessionMessageEntry
{
  /**
   * We keep messages in a doubly linked list.
   */
  struct SessionMessageEntry *next;

  /**
   * We keep messages in a doubly linked list.
   */
  struct SessionMessageEntry *prev;

  /**
   * How important is this message.
   */
  enum GNUNET_MQ_PriorityPreferences priority;

  /**
   * Deadline for transmission, 1s after we received it (if we
   * are not corking), otherwise "now".  Note that this message
   * does NOT expire past its deadline.
   */
  struct GNUNET_TIME_Absolute deadline;

  /**
   * How long is the message? (number of bytes following the `struct
   * MessageEntry`, but not including the size of `struct
   * MessageEntry` itself!)
   */
  size_t size;
};


/**
 * Data kept per session.
 */
struct Session
{
  /**
   * Identity of the other peer.
   */
  const struct GNUNET_PeerIdentity *peer;

  /**
   * Key exchange state for this peer.
   */
  struct GSC_KeyExchangeInfo *kx;

  /**
   * Head of list of requests from clients for transmission to
   * this peer.
   */
  struct GSC_ClientActiveRequest *active_client_request_head;

  /**
   * Tail of list of requests from clients for transmission to
   * this peer.
   */
  struct GSC_ClientActiveRequest *active_client_request_tail;

  /**
   * Head of list of messages ready for encryption.
   */
  struct SessionMessageEntry *sme_head;

  /**
   * Tail of list of messages ready for encryption.
   */
  struct SessionMessageEntry *sme_tail;

  // TODO
  // struct GSC_ServicesInfo *services;

  /**
   * Task to transmit corked messages with a delay.
   */
  struct GNUNET_SCHEDULER_Task *cork_task;

  /**
   * Class of the peer
   */
  enum GNUNET_CORE_PeerClass class;
};


/**
 * Map of peer identities to `struct Session`.
 */
static struct GNUNET_CONTAINER_MultiPeerMap *sessions;


/**
 * Find the session for the given peer.
 *
 * @param peer identity of the peer
 * @return NULL if we are not connected, otherwise the
 *         session handle
 */
static struct Session *
find_session (const struct GNUNET_PeerIdentity *peer)
{
  if (NULL == sessions)
    return NULL;
  return GNUNET_CONTAINER_multipeermap_get (sessions, peer);
}


/**
 * End the session with the given peer (we are no longer
 * connected).
 *
 * @param pid identity of peer to kill session with
 */
void
GSC_SESSIONS_end (const struct GNUNET_PeerIdentity *pid)
{
  struct Session *session;
  struct GSC_ClientActiveRequest *car;
  struct SessionMessageEntry *sme;

  if (NULL == pid)
  {
    /* We might not know the peer_id yet. */
    return;
  }
  session = find_session (pid);
  if (NULL == session)
    return;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Destroying session for peer `%s'\n",
              GNUNET_i2s (session->peer));
  if (NULL != session->cork_task)
  {
    GNUNET_SCHEDULER_cancel (session->cork_task);
    session->cork_task = NULL;
  }
  while (NULL != (car = session->active_client_request_head))
  {
    GNUNET_CONTAINER_DLL_remove (session->active_client_request_head,
                                 session->active_client_request_tail,
                                 car);
    GSC_CLIENTS_reject_request (car, GNUNET_NO);
  }
  while (NULL != (sme = session->sme_head))
  {
    GNUNET_CONTAINER_DLL_remove (session->sme_head, session->sme_tail, sme);
    GNUNET_free (sme);
  }
  // TODO
  GSC_CLIENTS_notify_clients_about_neighbour (session->peer,
                                              session->class);
  GNUNET_assert (
    GNUNET_YES ==
    GNUNET_CONTAINER_multipeermap_remove (sessions, session->peer, session));
  GNUNET_STATISTICS_set (GSC_stats,
                         gettext_noop ("# peers connected"),
                         GNUNET_CONTAINER_multipeermap_size (sessions),
                         GNUNET_NO);
  // TODO
  // GSC_SVCI_destroy (session->services);
  GNUNET_free (session);
}


/**
 * Create a session, a key exchange was just completed.
 *
 * @param peer peer that is now connected
 * @param kx key exchange that completed
 * @param class the class of the now connected peer
 */
void
GSC_SESSIONS_create (const struct GNUNET_PeerIdentity *peer,
                     struct GSC_KeyExchangeInfo *kx,
                     enum GNUNET_CORE_PeerClass class)
{
  struct Session *session;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Creating session for peer `%s'\n",
              GNUNET_i2s (peer));
  session = GNUNET_new (struct Session);
  // TODO
  // session->services = GSC_SVCI_init ();
  session->peer = peer;
  session->kx = kx;
  session->class = class;
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_CONTAINER_multipeermap_put (
                   sessions,
                   session->peer,
                   session,
                   GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
  GNUNET_STATISTICS_set (GSC_stats,
                         gettext_noop ("# peers connected"),
                         GNUNET_CONTAINER_multipeermap_size (sessions),
                         GNUNET_NO);
  // TODO
  GSC_CLIENTS_notify_clients_about_neighbour (peer,
                                              session->class);
}


/**
 * The other peer has indicated that it 'lost' the session
 * (KX down), reinitialize the session on our end, in particular
 * this means to restart the typemap transmission.
 * XXX typemap does not exist anymore
 *
 * @param peer peer that is now connected
 */
void
GSC_SESSIONS_reinit (const struct GNUNET_PeerIdentity *peer)
{
  struct Session *session;

  session = find_session (peer);
  if (NULL == session)
  {
    /* KX/session is new for both sides; thus no need to restart what
       has not yet begun */
    return;
  }
}


/**
 * Notify the given client about the session (client is new).
 *
 * @param cls the `struct GSC_Client`
 * @param key peer identity
 * @param value the `struct Session`
 * @return #GNUNET_OK (continue to iterate)
 */
static int
notify_client_about_session (void *cls,
                             const struct GNUNET_PeerIdentity *key,
                             void *value)
{
  struct GSC_Client *client = cls;
  struct Session *session = value;

  // TODO
  GSC_CLIENTS_notify_client_about_neighbour (client,
                                             session->peer,
                                             session->class);
  return GNUNET_OK;
}


/**
 * We have a new client, notify it about all current sessions.
 *
 * @param client the new client
 */
void
GSC_SESSIONS_notify_client_about_sessions (struct GSC_Client *client)
{
  /* notify new client about existing sessions */
  GNUNET_CONTAINER_multipeermap_iterate (sessions,
                                         &notify_client_about_session,
                                         client);
}


/**
 * Try to perform a transmission on the given session.  Will solicit
 * additional messages if the 'sme' queue is not full enough.
 *
 * @param session session to transmit messages from
 */
static void
try_transmission (struct Session *session);


/**
 * Queue a request from a client for transmission to a particular peer.
 *
 * @param car request to queue; this handle is then shared between
 *         the caller (CLIENTS subsystem) and SESSIONS and must not
 *         be released by either until either #GSC_SESSIONS_dequeue(),
 *         #GSC_SESSIONS_transmit() or #GSC_CLIENTS_failed()
 *         have been invoked on it
 */
void
GSC_SESSIONS_queue_request (struct GSC_ClientActiveRequest *car)
{
  struct Session *session;

  session = find_session (&car->target);
  if (NULL == session)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Dropped client request for transmission (am disconnected)\n");
    GNUNET_break (0);  /* should have been rejected earlier */
    GSC_CLIENTS_reject_request (car, GNUNET_NO);
    return;
  }
  if (car->msize > GNUNET_CONSTANTS_MAX_ENCRYPTED_MESSAGE_SIZE)
  {
    GNUNET_break (0);
    GSC_CLIENTS_reject_request (car, GNUNET_YES);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Received client transmission request. queueing\n");
  GNUNET_CONTAINER_DLL_insert_tail (session->active_client_request_head,
                                    session->active_client_request_tail,
                                    car);
  try_transmission (session);
}


/**
 * Dequeue a request from a client from transmission to a particular peer.
 *
 * @param car request to dequeue; this handle will then be 'owned' by
 *        the caller (CLIENTS sysbsystem)
 */
void
GSC_SESSIONS_dequeue_request (struct GSC_ClientActiveRequest *car)
{
  const struct GNUNET_PeerIdentity *my_identity;
  struct Session *session;
  my_identity = GNUNET_PILS_get_identity (GSC_pils);
  if (NULL == my_identity)
    return;

  if (0 == memcmp (&car->target,
                   my_identity,
                   sizeof(struct GNUNET_PeerIdentity)))
    return;
  session = find_session (&car->target);
  GNUNET_assert (NULL != session);
  GNUNET_CONTAINER_DLL_remove (session->active_client_request_head,
                               session->active_client_request_tail,
                               car);
  /* dequeueing of 'high' priority messages may unblock
     transmission for lower-priority messages, so we also
     need to try in this case. */
  try_transmission (session);
}


/**
 * Solicit messages for transmission, starting with those of the highest
 * priority.
 *
 * @param session session to solict messages for
 * @param msize how many bytes do we have already
 */
static void
solicit_messages (struct Session *session, size_t msize)
{
  struct GSC_ClientActiveRequest *car;
  struct GSC_ClientActiveRequest *nxt;
  size_t so_size;
  enum GNUNET_MQ_PriorityPreferences pmax;

  so_size = msize;
  pmax = GNUNET_MQ_PRIO_BACKGROUND;
  for (car = session->active_client_request_head; NULL != car; car = car->next)
  {
    if (GNUNET_YES == car->was_solicited)
      continue;
    pmax = GNUNET_MAX (pmax, car->priority & GNUNET_MQ_PRIORITY_MASK);
  }
  nxt = session->active_client_request_head;
  while (NULL != (car = nxt))
  {
    nxt = car->next;
    if (car->priority < pmax)
      continue;
    if (so_size + car->msize > GNUNET_CONSTANTS_MAX_ENCRYPTED_MESSAGE_SIZE)
      break;
    so_size += car->msize;
    if (GNUNET_YES == car->was_solicited)
      continue;
    car->was_solicited = GNUNET_YES;
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Soliciting message with priority %u\n",
                car->priority);
    GSC_CLIENTS_solicit_request (car);
    /* The above call may *dequeue* requests and thereby
       clobber 'nxt'. Hence we need to restart from the
       head of the list. */
    nxt = session->active_client_request_head;
    so_size = msize;
  }
}


/**
 * Some messages were delayed (corked), but the timeout has now expired.
 * Send them now.
 *
 * @param cls `struct Session` with the messages to transmit now
 */
static void
pop_cork_task (void *cls)
{
  struct Session *session = cls;

  session->cork_task = NULL;
  try_transmission (session);
}


/**
 * Try to perform a transmission on the given session. Will solicit
 * additional messages if the 'sme' queue is not full enough or has
 * only low-priority messages.
 *
 * @param session session to transmit messages from
 */
static void
try_transmission (struct Session *session)
{
  struct SessionMessageEntry *pos;
  size_t msize;
  struct GNUNET_TIME_Absolute now;
  struct GNUNET_TIME_Absolute min_deadline;
  enum GNUNET_MQ_PriorityPreferences maxp;
  enum GNUNET_MQ_PriorityPreferences maxpc;
  struct GSC_ClientActiveRequest *car;
  int excess;

  msize = 0;
  min_deadline = GNUNET_TIME_UNIT_FOREVER_ABS;
  /* if the peer has excess bandwidth, background traffic is allowed,
     otherwise not */
  if (MAX_ENCRYPTED_MESSAGE_QUEUE_SIZE <=
      GSC_NEIGHBOURS_get_queue_length (session->kx))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Transmission queue already very long, waiting...\n");
    return;   /* queue already too long */
  }
  excess = GSC_NEIGHBOURS_check_excess_bandwidth (session->kx);
  if (GNUNET_YES == excess)
    maxp = GNUNET_MQ_PRIO_BACKGROUND;
  else
    maxp = GNUNET_MQ_PRIO_BEST_EFFORT;
  /* determine highest priority of 'ready' messages we already solicited from clients */
  pos = session->sme_head;
  while ((NULL != pos) &&
         (msize + pos->size <= GNUNET_CONSTANTS_MAX_ENCRYPTED_MESSAGE_SIZE))
  {
    GNUNET_assert (pos->size < GNUNET_CONSTANTS_MAX_ENCRYPTED_MESSAGE_SIZE);
    msize += pos->size;
    maxp = GNUNET_MAX (maxp, pos->priority & GNUNET_MQ_PRIORITY_MASK);
    min_deadline = GNUNET_TIME_absolute_min (min_deadline, pos->deadline);
    pos = pos->next;
  }
  GNUNET_log (
    GNUNET_ERROR_TYPE_DEBUG,
    "Calculating transmission set with %u priority (%s) and %s earliest deadline\n",
    maxp,
    (GNUNET_YES == excess) ? "excess bandwidth" : "limited bandwidth",
    GNUNET_STRINGS_relative_time_to_string (GNUNET_TIME_absolute_get_remaining (
                                              min_deadline),
                                            GNUNET_YES));

  if (maxp < GNUNET_MQ_PRIO_CRITICAL_CONTROL)
  {
    /* if highest already solicited priority from clients is not critical,
       check if there are higher-priority messages to be solicited from clients */
    if (GNUNET_YES == excess)
      maxpc = GNUNET_MQ_PRIO_BACKGROUND;
    else
      maxpc = GNUNET_MQ_PRIO_BEST_EFFORT;
    for (car = session->active_client_request_head; NULL != car;
         car = car->next)
    {
      if (GNUNET_YES == car->was_solicited)
        continue;
      maxpc = GNUNET_MAX (maxpc, car->priority & GNUNET_MQ_PRIORITY_MASK);
    }
    if (maxpc > maxp)
    {
      /* we have messages waiting for solicitation that have a higher
         priority than those that we already accepted; solicit the
         high-priority messages first */
      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                  "Soliciting messages based on priority (%u > %u)\n",
                  maxpc,
                  maxp);
      solicit_messages (session, 0);
      return;
    }
  }
  else
  {
    /* never solicit more, we have critical messages to process */
    excess = GNUNET_NO;
    maxpc = GNUNET_MQ_PRIO_BACKGROUND;
  }
  now = GNUNET_TIME_absolute_get ();
  if (((GNUNET_YES == excess) || (maxpc >= GNUNET_MQ_PRIO_BEST_EFFORT)) &&
      ((0 == msize) ||
       ((msize < GNUNET_CONSTANTS_MAX_ENCRYPTED_MESSAGE_SIZE / 2) &&
        (min_deadline.abs_value_us > now.abs_value_us))))
  {
    /* not enough ready yet (tiny message & cork possible), or no messages at all,
       and either excess bandwidth or best-effort or higher message waiting at
       client; in this case, we try to solicit more */
    GNUNET_log (
      GNUNET_ERROR_TYPE_DEBUG,
      "Soliciting messages (excess %d, maxpc %d, message size %u, deadline %s)\n",
      excess,
      maxpc,
      (unsigned int) msize,
      GNUNET_STRINGS_relative_time_to_string (
        GNUNET_TIME_absolute_get_remaining (
          min_deadline),
        GNUNET_YES));
    solicit_messages (session, msize);
    if (msize > 0)
    {
      /* if there is data to send, just not yet, make sure we do transmit
       * it once the deadline is reached */
      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                  "Corking until %s\n",
                  GNUNET_STRINGS_relative_time_to_string (
                    GNUNET_TIME_absolute_get_remaining (min_deadline),
                    GNUNET_YES));
      if (NULL != session->cork_task)
        GNUNET_SCHEDULER_cancel (session->cork_task);
      session->cork_task =
        GNUNET_SCHEDULER_add_at (min_deadline, &pop_cork_task, session);
    }
    else
    {
      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                  "Queue empty, waiting for solicitations\n");
    }
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Building combined plaintext buffer to transmit message!\n");
  /* create plaintext buffer of all messages (that fit), encrypt and
     transmit */
  {
    static unsigned long long total_bytes;
    static unsigned int total_msgs;
    char pbuf[msize]; /* plaintext */
    size_t used;

    used = 0;
    while ((NULL != (pos = session->sme_head)) && (used + pos->size <= msize))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                  "Adding message of type %d to payload for %s\n",
                  ntohs (((const struct GNUNET_MessageHeader *) &pos[1])->type),
                  GNUNET_i2s (session->peer));
      GNUNET_memcpy (&pbuf[used], &pos[1], pos->size);
      used += pos->size;
      GNUNET_CONTAINER_DLL_remove (session->sme_head, session->sme_tail, pos);
      GNUNET_free (pos);
    }
    /* compute average payload size */
    total_bytes += used;
    total_msgs++;
    if (0 == total_msgs)
    {
      /* 2^32 messages, wrap around... */
      total_msgs = 1;
      total_bytes = used;
    }
    GNUNET_STATISTICS_set (GSC_stats,
                           "# avg payload per encrypted message",
                           total_bytes / total_msgs,
                           GNUNET_NO);
    /* now actually transmit... */
    GSC_KX_encrypt_and_transmit (session->kx, pbuf, used);
  }
}


/**
 * Traffic is being solicited for the given peer.  This means that the
 * message queue on the transport-level (NEIGHBOURS subsystem) is now
 * empty and it is now OK to transmit another (non-control) message.
 *
 * @param pid identity of peer ready to receive data
 */
void
GSC_SESSIONS_solicit (const struct GNUNET_PeerIdentity *pid)
{
  struct Session *session;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Transport solicits for %s\n",
              GNUNET_i2s (pid));
  session = find_session (pid);
  if (NULL == session)
    return;
  try_transmission (session);
}


void
GSC_SESSIONS_transmit (struct GSC_ClientActiveRequest *car,
                       const struct GNUNET_MessageHeader *msg,
                       enum GNUNET_MQ_PriorityPreferences priority)
{
  struct Session *session;
  struct SessionMessageEntry *sme;
  struct SessionMessageEntry *pos;
  size_t msize;

  session = find_session (&car->target);
  if (NULL == session)
  {
    GSC_CLIENTS_reject_request (car, GNUNET_YES);
    return;
  }
  msize = ntohs (msg->size);
  sme = GNUNET_malloc (sizeof(struct SessionMessageEntry) + msize);
  GNUNET_memcpy (&sme[1], msg, msize);
  sme->size = msize;
  sme->priority = priority;
  if (0 != (GNUNET_MQ_PREF_CORK_ALLOWED & priority))
  {
    sme->deadline =
      GNUNET_TIME_relative_to_absolute (GNUNET_CONSTANTS_MAX_CORK_DELAY);
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Message corked, delaying transmission\n");
  }
  pos = session->sme_head;
  while ((NULL != pos) && (pos->priority >= sme->priority))
    pos = pos->next;
  if (NULL == pos)
    GNUNET_CONTAINER_DLL_insert_tail (session->sme_head,
                                      session->sme_tail,
                                      sme);
  else
    GNUNET_CONTAINER_DLL_insert_after (session->sme_head,
                                       session->sme_tail,
                                       pos->prev,
                                       sme);
  try_transmission (session);
}


/**
 * Initialize sessions subsystem.
 */
void
GSC_SESSIONS_init ()
{
  sessions = GNUNET_CONTAINER_multipeermap_create (128, GNUNET_YES);
}


/**
 * Helper function for #GSC_SESSIONS_done() to free all
 * active sessions.
 *
 * @param cls NULL
 * @param key identity of the connected peer
 * @param value the `struct Session` for the peer
 * @return #GNUNET_OK (continue to iterate)
 */
static int
free_session_helper (void *cls,
                     const struct GNUNET_PeerIdentity *key,
                     void *value)
{
  /* struct Session *session = value; */

  GSC_SESSIONS_end (key);
  return GNUNET_OK;
}


/**
 * Shutdown sessions subsystem.
 */
void
GSC_SESSIONS_done ()
{
  if (NULL != sessions)
  {
    GNUNET_CONTAINER_multipeermap_iterate (sessions,
                                           &free_session_helper,
                                           NULL);
    GNUNET_CONTAINER_multipeermap_destroy (sessions);
    sessions = NULL;
  }
}


/* end of gnunet-service-core_sessions.c */
