
// RPC over AMQP

#include "rpc_amqp.hpp"
#include "config.hpp"

#include "rpc_amqp_utils.hpp"
#include "json_service_call.hpp"

#include <stdexcept>
#include <string>

#include <stdio.h>
#include <syslog.h>

#include <amqp_tcp_socket.h>
#include <amqp.h>
#include <amqp_framing.h>

#include "io.hpp"

using namespace std;

// error handling


void throw_ex_on_amqp_error(amqp_rpc_reply_t rc, amqp_connection_state_t conn, amqp_channel_t channel, char const *context)
{

   std::string ct(context);

   if(rc.reply_type != AMQP_RESPONSE_NORMAL)
   {
      amqp_rpc_reply_t rc_ch_close = amqp_channel_close(conn, channel, AMQP_REPLY_SUCCESS);
      if(rc_ch_close.reply_type != AMQP_RESPONSE_NORMAL)
         throw std::runtime_error("cannot close channel after unsuccessful " + ct);

      amqp_rpc_reply_t rc_conn_close = amqp_connection_close(conn, AMQP_REPLY_SUCCESS);
      if(rc_conn_close.reply_type != AMQP_RESPONSE_NORMAL)
         throw std::runtime_error("cannot close connection after unsuccessful " + ct);

      if(AMQP_STATUS_OK != amqp_destroy_connection(conn))
         throw std::runtime_error("cannot end connection after unsuccessful " + ct);
      else
         throw std::runtime_error(ct + " failed");
   }
}



void syslog_on_amqp_error(amqp_rpc_reply_t x, char const *context)
{
   switch (x.reply_type) {
      case AMQP_RESPONSE_NORMAL:
         return;

      case AMQP_RESPONSE_NONE:
         syslog(LOG_ERR, "%s: missing RPC reply type!\n", context);
         break;

      case AMQP_RESPONSE_LIBRARY_EXCEPTION:
         syslog(LOG_ERR, "%s: %s\n", context, amqp_error_string2(x.library_error));
         break;

      case AMQP_RESPONSE_SERVER_EXCEPTION:
         switch (x.reply.id) {
            case AMQP_CONNECTION_CLOSE_METHOD:
               {
                  amqp_connection_close_t *m =
                     (amqp_connection_close_t *)x.reply.decoded;
                  syslog(LOG_ERR, "%s: server connection error %uh, message: %.*s\n",
                        context, m->reply_code, (int)m->reply_text.len,
                        (char *)m->reply_text.bytes);
                  break;
               }
            case AMQP_CHANNEL_CLOSE_METHOD:
               {
                  amqp_channel_close_t *m = (amqp_channel_close_t *)x.reply.decoded;
                  syslog(LOG_ERR, "%s: server channel error %uh, message: %.*s\n",
                        context, m->reply_code, (int)m->reply_text.len,
                        (char *)m->reply_text.bytes);
                  break;
               }
            default:
               syslog(LOG_ERR, "%s: unknown server error, method id 0x%08X\n",
                     context, x.reply.id);
               break;
         }
         break;
   }
}



// AMQP RPC
//
// establish connection to RabbitMQ-broker on "conn" and channel=1
// use this connection [conn,channel] to:
// * create queue where Java-vlkb-client will put messages (queuename must match routingKey of Java-client config file)
// * bind the queue to pre-defined exchange "amq.direct"
// * ask the broker to start basic-consumer on that queue
// WAIT: Consume message from "conn"
// Create new reply-message with CorrdId from received message
// * publish the reply-msg to reply-to queue
// return to WAIT: ... loop forever

amqp_connection_state_t login_to_broker(const string user_name, const string password,
      const string hostname, int port)
{
   // allocate new conn and initialize
   // NOTE: must destroy conn at exit

 amqp_connection_state_t  conn = amqp_new_connection();
   if(conn == NULL)
      throw std::runtime_error("cannot create new connection");


   { // open new TCP-socket and store in conn

      amqp_socket_t *socket = NULL;
      socket = amqp_tcp_socket_new(conn);
      if (socket == NULL)
      {
         if(AMQP_STATUS_OK != amqp_destroy_connection(conn))
            throw std::runtime_error("cannot end connection after unsuccessful new TCP socket");
         else
            throw std::runtime_error("error creating TCP socket");
      }
      int status;
      status = amqp_socket_open(socket, hostname.c_str(), port);
      if (status != 0)
      {
         if(AMQP_STATUS_OK != amqp_destroy_connection(conn))
            throw std::runtime_error("cannot end connection after unsuccessful socket open");
         else 
            throw std::runtime_error("error opening TCP socket");// FIXME add status to msg
      }
   }


   amqp_rpc_reply_t rc;
   rc = amqp_login(conn, "/", 0, 131072, 0, AMQP_SASL_METHOD_PLAIN, user_name.c_str(), password.c_str());
   if(rc.reply_type != AMQP_RESPONSE_NORMAL)
   {
      amqp_rpc_reply_t rc_close = amqp_connection_close(conn, AMQP_REPLY_SUCCESS);
      if(rc_close.reply_type != AMQP_RESPONSE_NORMAL)
         throw std::runtime_error("cannot close connection after unsuccessful amqp login");
      else if(AMQP_STATUS_OK != amqp_destroy_connection(conn))
         throw std::runtime_error("cannot end connection after unsuccessful amqp login");
      else
         throw std::runtime_error("amqp_login failed");
   }

   return conn;
}



// RPC-loop



int channel_open(amqp_connection_state_t conn, amqp_channel_t channel)
{
   amqp_channel_open(conn, channel);
   amqp_rpc_reply_t rep = amqp_get_rpc_reply(conn);

   return (rep.reply_type != AMQP_RESPONSE_NORMAL);
}



void declare_nondurable_autodelete_queue(
      amqp_connection_state_t conn, amqp_channel_t channel,
      amqp_bytes_t queuename)
{
   amqp_queue_declare(conn, channel,
         queuename,
         0, // 'passive' guarantees that this client sees the queue which was created already
         0, // 'durable' queue survives broker restarts
         0, // 'exclusive' to current connection (queue deleted when conn closes)
         1, // 'auto_delete' the queue when not used
         amqp_empty_table); // amqp_table_t arguments specific for AMQP broker implementation (none in RabbitMQ)
}



// start a queue consumer (e.g. start delivering msgs from the queue to this client)
// broker-implementation should support at least 16 consumers per queue
void start_basic_consumer_noack(
      amqp_connection_state_t conn, amqp_channel_t channel,
      amqp_bytes_t queuename)
{
   amqp_basic_consume(conn, channel,
         queuename,
         amqp_empty_bytes, // consumer_tag amqp_bytes_t: consumer-identifier (if empty, server generates a tag)
         0,  // no_local amqp_boolean_t: broker will not send msgs to connection which published them
         1,  // no_ack amqp_boolean_t: broker does not expect acknowledgement for delivered msgs
         0,  // exclusive  amqp_boolean_t : only this consumer can access the queue
         amqp_empty_table); // arguments amqp_table_t: implementation specific args (not used in RabbitMQ)
} 



int consume_message_wait_forever(amqp_connection_state_t conn, amqp_envelope_t *envelope)
{
   // release memory associated with all channels
   amqp_maybe_release_buffers(conn);

   amqp_rpc_reply_t res = amqp_consume_message(conn,
         envelope, // message in envelope
         NULL,     // timeout (struct *)
         0);       // flags (int) AMQP_UNUSED

   if (AMQP_RESPONSE_NORMAL != res.reply_type)
   {
      syslog_on_amqp_error(res, "amqp_consume_message");
   }

   return (AMQP_RESPONSE_NORMAL != res.reply_type);
}



void basic_publish_on_queue_or_drop(amqp_connection_state_t conn, amqp_channel_t channel,
      amqp_bytes_t queuename,
      amqp_bytes_t correlation_id,
      const char * msg_buff)
{
   amqp_basic_properties_t props;

   props._flags =
      AMQP_BASIC_CONTENT_TYPE_FLAG  |
      AMQP_BASIC_DELIVERY_MODE_FLAG |
      AMQP_BASIC_CORRELATION_ID_FLAG;
   props.content_type   = amqp_cstring_bytes("application/json");// FIXME make sure encoding is UTF-8
   //props.content_type   = amqp_cstring_bytes("text/plain");
   props.delivery_mode  = 2;
   // 1: non-persistent
   // 2: persistent (delivered even if broker re-boots - msg held on hard disk)
   props.correlation_id = correlation_id;

   int rc = amqp_basic_publish(conn, channel,
         amqp_empty_bytes, // exchange amqp_bytes_t: empty = default-exchange
         queuename,        // routingKey := queuename  amqp_bytes_t
         0, // mandatory amqp_boolean_t 0: drop the msg if cannot be routed (1: return msg)
         0, // immediate amqp_boolean_t 0: queue the msg if cannot be routed immediately (1: -"-)
         &props, // amqp_basic_properties_t
         amqp_cstring_bytes(msg_buff)); // body

   if (rc < 0)
   {
      syslog(LOG_ERR, "%s: basic publish failed with %uh, message: %s\n",__func__, rc, amqp_error_string2(rc));
   }
}



// run RPC-loop
// even if error happens on consume-request or publish-response
void rpc_loop_forever(
      amqp_connection_state_t conn, amqp_channel_t channel,
      const string queuename,
      const string settings_pathname)
{
   if(channel_open(conn, channel))
   {
      amqp_rpc_reply_t rep_close = amqp_connection_close(conn, AMQP_REPLY_SUCCESS);
      if(rep_close.reply_type != AMQP_RESPONSE_NORMAL)
         throw std::runtime_error("cannot close connection after unsuccessful channel open");
      else if(AMQP_STATUS_OK != amqp_destroy_connection(conn))
         throw std::runtime_error("cannot end connection after unsuccessful channel open");
      else
         throw std::runtime_error("channel open failed");
   }


   declare_nondurable_autodelete_queue(conn, channel, amqp_cstring_bytes(queuename.c_str()));
   throw_ex_on_amqp_error(amqp_get_rpc_reply(conn), conn, channel, "amqp queue declare");

   start_basic_consumer_noack(conn, channel, amqp_cstring_bytes(queuename.c_str()));
   throw_ex_on_amqp_error(amqp_get_rpc_reply(conn), conn, channel, "amqp basic consume");

   syslog(LOG_INFO,"AMQP initialized. Run RPC loop.");

   config conf;
   conf.read_config(settings_pathname);
   syslog(LOG_INFO, string("Will log to " + conf.getLogDir()).c_str());

   for (;;)
   {
      amqp_envelope_t envelope;

      if(consume_message_wait_forever(conn, &envelope))
      {
         continue;
      }

      string request_json((const char*)envelope.message.body.bytes, envelope.message.body.len);

      // RPC call

      LOG_open(conf.getLogDir(), conf.getLogFileName());

      string reply_json;
      try
      {
         reply_json = service_call(request_json, queuename, conf);
      }
      catch(const invalid_argument& ex)
      {
         reply_json = service_exception(service_error::INVALID_PARAM, ex.what());
      }
      catch(const exception& ex)
      {
         reply_json = service_exception(service_error::SYSTEM_ERROR, ex.what());
      }

      LOG_close();


      basic_publish_on_queue_or_drop(conn, channel,
            envelope.message.properties.reply_to,
            envelope.message.properties.correlation_id,
            reply_json.c_str());

      amqp_destroy_envelope(&envelope);
   }

   // Function never returns. Terminate with signal.
}



void do_cleanup(amqp_connection_state_t conn, amqp_channel_t channel)
{
   die_on_amqp_error(amqp_channel_close(conn, channel, AMQP_REPLY_SUCCESS), "Closing channel");
   die_on_amqp_error(amqp_connection_close(conn, AMQP_REPLY_SUCCESS), "Closing connection");
   die_on_error(amqp_destroy_connection(conn), "Ending connection");

   LOG_close();
}



// interfaces

// global to make accessible from signal_handler FIXME
amqp_connection_state_t conn;
amqp_channel_t channel;


void rpc_run(const string user_name, const string password,
      const string hostname, int port,
      const string rpc_queuename,
      const string settings_pathname)
{
   conn = login_to_broker(user_name, password, hostname, port);

   channel = 1; // only single AMQP-channel per connection needed, use channel no. 1
   rpc_loop_forever(conn, channel, rpc_queuename, settings_pathname); // func never returns
}


void rpc_cleanup(void)
{
   do_cleanup(conn, channel);
}










///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// NOTE:
// this was in rpc_run_loop AFTER queue_declare and BEFORE basic_consume :
#ifdef usedefaultexchange
// bind queue to exchange

amqp_queue_bind(conn, channel,
      queuename,
      amqp_cstring_bytes("amq.direct"), // exchange
      queuename,                        // routingKey := queuename
      amqp_empty_table);                // empty arguments
throw_ex_on_amqp_error(amqp_get_rpc_reply(conn), "amqp queue bind");

// better load balancing

amqp_basic_qos_ok_t * qok = amqp_basic_qos(conn, channel,
      0, // prefetch_size   uint32_t
      1, // prefetch_count  uint16_t
      0);// global    amqp_boolean_t :
// =0 prefetch_count applies seperatly to each consumer
// =1 prefetch_count applices to all consumers
throw_ex_on_amqp_error(amqp_get_rpc_reply(conn), "amqp basic QoS");
#endif
// ask the broker to start a basic-consumer on queue "queuename"
// serves all channels in connection (envelope.channel) -> always reply to
// queue whos name is in reply-to field (no need to ditinguish channels,
// reply-to queues were created by that channel)

// no_ack affects message consume from queue:
// broker will remove msg right after delivery without waiting for confirmation from connected peer
// improves performance on expense of reliability



/* util 
void print_envelope_if(int condition, amqp_envelope_t * envelope)
{
   if(condition){
      printf("Delivery %u, exchange %.*s routingkey %.*s\n",
            (unsigned) envelope->delivery_tag,
            (int) envelope->exchange.len, (char *) envelope->exchange.bytes,
            (int) envelope->routing_key.len, (char *) envelope->routing_key.bytes);

      if (envelope->message.properties._flags & AMQP_BASIC_CONTENT_TYPE_FLAG) {
         printf("Content-type: %.*s\n",
               (int) envelope->message.properties.content_type.len,
               (char *) envelope->message.properties.content_type.bytes);
      }
      printf("----\n");
      //        amqp_dump(envelope->message.body.bytes, envelope->message.body.len);
   }
}
*/

