//
// Copyright (c) 2017 Wind River Systems, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <list>
#include <new>
#include <vector>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>

#include "fmAPI.h"
#include "fmMsg.h"
#include "fmLog.h"
#include "fmSocket.h"
#include "fmMutex.h"
#include "fmThread.h"
#include "fmAlarmUtils.h"


#define FM_MGR_HOST_NAME "controller"
#define MAX_PENDING_REQUEST 1000

#define HANDLE_SERVER_RC(hdr) \
  if ((hdr)->msg_rc!=FM_ERR_OK) return (EFmErrorT) (hdr)->msg_rc

#define CHECK_RESPONSE(hdr,neededstruct) \
  if (!fm_valid_srv_msg(hdr,sizeof(neededstruct)))  \
    return FM_ERR_COMMUNICATIONS

#define CHECK_LIST_FULL(l) \
  if (l.size() == MAX_PENDING_REQUEST)          \
    return FM_ERR_NOT_ENOUGH_SPACE

#define CHECK_LIST_NOT_EMPTY(l) \
  if (l.size() != 0)                            \
    return FM_ERR_REQUEST_PENDING

static CFmSocket m_client;
static bool m_connected = false;
static bool m_thread = false;

typedef std::list<fm_buff_t>  FmRequestListT;


static FmRequestListT & GetListOfFmRequests() {
  static FmRequestListT reqs;
  return reqs;
}


static CFmMutex & getListMutex() {
  static CFmMutex *m = new CFmMutex;
  return *m;
}


static CFmMutex & getThreadMutex() {
  static CFmMutex *m = new CFmMutex;
  return *m;
}


CFmMutex & getAPIMutex() {
  static pthread_mutex_t ml = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
  static CFmMutex *m = NULL;
  if (m == NULL) {
    pthread_mutex_lock(&ml);
    m = new CFmMutex;
    pthread_mutex_unlock(&ml);
  }
  return *m;
}


static void enqueue(fm_buff_t &req) {
  CFmMutexGuard m(getListMutex());
  GetListOfFmRequests().push_back(req);
}


static bool dequeue(fm_buff_t &req) {
  CFmMutexGuard m(getListMutex());
  if (GetListOfFmRequests().size() == 0) {
    return false;
  }

  FmRequestListT::iterator it = GetListOfFmRequests().begin();
  req.clear();
  req = (*it);
  GetListOfFmRequests().pop_front();
  return true;
}


static bool fm_lib_reconnect() {
  char addr[INET6_ADDRSTRLEN];

  while (!m_connected) {
    struct addrinfo hints;
    struct addrinfo *result = NULL, *rp;
    memset(&hints,0,sizeof(hints));
    hints.ai_family = AF_UNSPEC;     /* Allow IPv4 or IPv6 */
    hints.ai_socktype = SOCK_STREAM; /* Datagram socket */
    hints.ai_flags = 0;              /* For wildcard IP address */
    hints.ai_protocol = 0;           /* Any protocol */
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;

    int rc = getaddrinfo(FM_MGR_HOST_NAME, NULL,
                         &hints,
                         &result);
    if (rc != 0) {
      FM_ERROR_LOG("controller lookup failed... errno:%d", errno);
      break;
    } else {
      for (rp = result; rp != NULL; rp = rp->ai_next) {
        if (rp->ai_family == AF_INET || rp->ai_family==AF_INET6) {
          if(rp->ai_family == AF_INET) {
            inet_ntop(AF_INET, &(((sockaddr_in*)rp->ai_addr)->sin_addr), addr, sizeof(addr));
          } else if (rp->ai_family == AF_INET6) {
            inet_ntop(AF_INET6, &(((sockaddr_in6*)rp->ai_addr)->sin6_addr), addr, sizeof(addr));
          }
          m_connected = m_client.connect(addr, 8001, rp->ai_family);
          if (m_connected == true) {
            FM_INFO_LOG("Connected to FM Manager.");
            break;
          } else {
            FM_WARNING_LOG("Failed to connect to FM Manager.");
          }
        }
      }
      freeaddrinfo(result);
    }
    break;
  }

  return (m_connected);
}


EFmErrorT fm_msg_utils_prep_requet_msg(fm_buff_t &buff,
                                       EFmMsgActionsT act,
                                       const void * data,
                                       uint32_t len) {

  try {
    buff.resize(sizeof(SFmMsgHdrT) + len);
  } catch (...) {
    FM_ERROR_LOG("Buff resize failed: errno:%d",errno);
    return FM_ERR_NOMEM;
  }
  SFmMsgHdrT *hdr = ptr_to_hdr(buff);
  hdr->action = act;
  hdr->msg_size = len;
  hdr->version = EFmMsgV1;
  hdr->msg_rc = 0;
  memcpy(ptr_to_data(buff), data, len);
  return FM_ERR_OK;
}


static void fmApiJobHandlerThread(void *context) {

  while (true) {
    fm_buff_t buff;
    buff.clear();
    while (dequeue(buff)) {
      while (true) {
        while (!fm_lib_reconnect()) {
          fmThreadSleep(200);
        }
        fm_log_request(buff);
        // protect from other sync APIs to access the same socket
        CFmMutexGuard m(getAPIMutex());
        if(m_client.write_packet(buff)) {
          fm_buff_t in_buff;
          in_buff.clear();
          if(!m_client.read_packet(in_buff)) {
            // retry after read failure
            fm_log_response(buff, in_buff, true);
            m_connected = false;
            continue;
          } else {
            fm_log_response(buff, in_buff);
            break;
          }
        } else {
          // retry after write failure
          fm_log_request(buff, true);
          m_connected = false;
          continue;
        }
      }
    }
    fmThreadSleep(50);
  }
}


static bool fm_lib_thread() {
  CFmMutexGuard m(getThreadMutex());
  if (!m_thread) {
    FM_INFO_LOG("Creating thread");
    if (!fmCreateThread(fmApiJobHandlerThread, NULL)) {
      FM_ERROR_LOG("Fail to create API job thread");
    } else {
      m_thread = true;
    }
  }
  return m_thread;
}


static EFmErrorT fm_check_thread_pending_request() {
  CFmMutexGuard m(getThreadMutex());
  if (m_thread) {
    CHECK_LIST_NOT_EMPTY(GetListOfFmRequests());
  }
  return FM_ERR_OK;
}

extern "C" {

EFmErrorT fm_init_lib() {
  signal(SIGINT,SIG_IGN);

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) {
    FM_ERROR_LOG("Socket connection failed\n");
    return FM_ERR_NOCONNECT;
  }
  return FM_ERR_OK;
}


EFmErrorT fm_set_fault(const SFmAlarmDataT *alarm,
                       fm_uuid_t *uuid) {

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) return FM_ERR_NOCONNECT;

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmCreateFault,
                                               alarm,sizeof(*alarm));
  if (erc!=FM_ERR_OK) return erc;

  if (m_client.write_packet(buff)) {
    if (!m_client.read_packet(buff)) {
      m_connected = false;
      return FM_ERR_NOCONNECT;
    }

    HANDLE_SERVER_RC(ptr_to_hdr(buff));
    CHECK_RESPONSE(ptr_to_hdr(buff),fm_uuid_t);
    if (uuid != NULL)
      memcpy(*uuid,ptr_to_data(buff),sizeof(*uuid)-1);
  } else {
    m_connected = false;
    return FM_ERR_NOCONNECT;
  }

  return FM_ERR_OK;
}


EFmErrorT fm_clear_fault(AlarmFilter *filter) {

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) return FM_ERR_NOCONNECT;

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmDeleteFault,
                                               filter, sizeof(*filter));
  if (erc!=FM_ERR_OK) return erc;

  if (m_client.write_packet(buff)) {
    if (!m_client.read_packet(buff)) {
      m_connected = false;
      return FM_ERR_NOCONNECT;
    }
    HANDLE_SERVER_RC(ptr_to_hdr(buff));
  } else {
    m_connected = false;
    return FM_ERR_NOCONNECT;
  }
  return FM_ERR_OK;
}


EFmErrorT fm_clear_all(fm_ent_inst_t *inst_id) {

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) return FM_ERR_NOCONNECT;

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmDeleteFaults,
                                               (*inst_id), sizeof(*inst_id));
  if (erc!=FM_ERR_OK) return erc;

  if (m_client.write_packet(buff)) {
    if (!m_client.read_packet(buff)) {
      m_connected = false;
      FM_ERROR_LOG("Read ERR: return FM_ERR_NOCONNECT");
      return FM_ERR_NOCONNECT;
    }
    HANDLE_SERVER_RC(ptr_to_hdr(buff));
  } else {
    m_connected = false;
    FM_ERROR_LOG("Write ERR: return FM_ERR_NOCONNECT");
    return FM_ERR_NOCONNECT;
  }
  return FM_ERR_OK;
}


EFmErrorT fm_get_fault(AlarmFilter *filter, SFmAlarmDataT *alarm ) {

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) return FM_ERR_NOCONNECT;
  fm_check_thread_pending_request();

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmGetFault,
                                               filter,sizeof(*filter));
  if (erc!=FM_ERR_OK) return erc;

  if (m_client.write_packet(buff)) {
    if (!m_client.read_packet(buff)) {
      m_connected = false;
      return FM_ERR_NOCONNECT;
    }
    HANDLE_SERVER_RC(ptr_to_hdr(buff));
    CHECK_RESPONSE(ptr_to_hdr(buff),SFmAlarmDataT);
    SFmAlarmDataT * data = (SFmAlarmDataT * ) ptr_to_data(buff);
    *alarm = *data;
  } else {
    m_connected = false;
    return FM_ERR_NOCONNECT;
  }

  return FM_ERR_OK;
}


EFmErrorT fm_get_faults(fm_ent_inst_t *inst_id,
                        SFmAlarmDataT *alarm,
                        unsigned int *max_alarms_to_get) {

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) return FM_ERR_NOCONNECT;
  fm_check_thread_pending_request();

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmGetFaults,
                                               (*inst_id),sizeof(*inst_id));
  if (erc!=FM_ERR_OK) return erc;

  if (m_client.write_packet(buff)) {
    if(!m_client.read_packet(buff)) {
      m_connected = false;
      return FM_ERR_NOCONNECT;
    }

    if (ptr_to_hdr(buff)->msg_rc != FM_ERR_OK) {
      *max_alarms_to_get = 0;
      EFmErrorT rc = (EFmErrorT)ptr_to_hdr(buff)->msg_rc;
      return rc;
    }

    uint32_t pkt_size = ptr_to_hdr(buff)->msg_size;
    if (pkt_size < sizeof(uint32_t)) {
      FM_ERROR_LOG("Received invalid pkt size: %u\n",pkt_size );
      m_connected = false;
      return FM_ERR_COMMUNICATIONS;
    }
    pkt_size-=sizeof(uint32_t);

    char *dptr = (char*)ptr_to_data(buff);

    uint32_t *len = (uint32_t*)dptr;
    dptr+=sizeof(uint32_t);
    if (*max_alarms_to_get < *len) {
      return FM_ERR_NOT_ENOUGH_SPACE;
    }
    if (pkt_size < (*len*sizeof(SFmAlarmDataT))) {
      return FM_ERR_COMMUNICATIONS;
    }
    *max_alarms_to_get = *len;
    memcpy(alarm,dptr,pkt_size);
  } else {
    m_connected = false;
    return FM_ERR_NOCONNECT;
  }

  return FM_ERR_OK;
}


EFmErrorT fm_get_faults_by_id(fm_alarm_id *alarm_id,
                              SFmAlarmDataT *alarm,
                              unsigned int *max_alarms_to_get) {

  CFmMutexGuard m(getAPIMutex());
  if (!fm_lib_reconnect()) return FM_ERR_NOCONNECT;
  fm_check_thread_pending_request();

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmGetFaultsById,
                                               (*alarm_id), sizeof(*alarm_id));
  if (erc!=FM_ERR_OK) return erc;

  if (m_client.write_packet(buff)) {
    if (!m_client.read_packet(buff)) {
      m_connected = false;
      return FM_ERR_NOCONNECT;
    }

    if (ptr_to_hdr(buff)->msg_rc != FM_ERR_OK){
      *max_alarms_to_get = 0;
      EFmErrorT rc = (EFmErrorT)ptr_to_hdr(buff)->msg_rc;
      return rc;
    }

    uint32_t pkt_size = ptr_to_hdr(buff)->msg_size;
    if (pkt_size < sizeof(uint32_t)) {
      FM_ERROR_LOG("Received invalid pkt size: %u\n",pkt_size );
      m_connected = false;
      return FM_ERR_COMMUNICATIONS;
    }
    pkt_size-=sizeof(uint32_t);

    char *dptr = (char*)ptr_to_data(buff);

    uint32_t *len = (uint32_t*)dptr;
    dptr+=sizeof(uint32_t);
    if (*max_alarms_to_get < *len) {
      return FM_ERR_NOT_ENOUGH_SPACE;
    }
    if (pkt_size < (*len*sizeof(SFmAlarmDataT)) ) {
      return FM_ERR_COMMUNICATIONS;
    }
    *max_alarms_to_get = *len;
    memcpy(alarm,dptr,pkt_size);
  } else {
    m_connected = false;
    return FM_ERR_NOCONNECT;
  }

  return FM_ERR_OK;
}


/*
 * APIs that enqueue the request and return ok for acknowledgment.
 * A backgroup thread will pick up the request and send it to the FM Manager
 */
EFmErrorT fm_set_fault_async(const SFmAlarmDataT *alarm, fm_uuid_t *uuid) {

  if ( !fm_lib_thread()) return FM_ERR_RESOURCE_UNAVAILABLE;
  CHECK_LIST_FULL(GetListOfFmRequests());

  fm_uuid_t id;
  fm_buff_t buff;
  buff.clear();
  fm_uuid_create(id);
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmCreateFault,
                                               alarm, sizeof(*alarm));
  if (erc != FM_ERR_OK) return erc;
  memcpy(ptr_to_data(buff), id, sizeof(fm_uuid_t)-1);

  FM_INFO_LOG("Enqueue raise alarm request: UUID (%s) alarm id (%s) instant id (%s)",
              id, alarm->alarm_id, alarm->entity_instance_id);
  enqueue(buff);

  if (uuid != NULL) {
    memcpy(*uuid,id,sizeof(*uuid)-1);
  }
  return FM_ERR_OK;
}


EFmErrorT fm_clear_fault_async(AlarmFilter *filter) {

  if ( !fm_lib_thread()) return FM_ERR_RESOURCE_UNAVAILABLE;
  CHECK_LIST_FULL(GetListOfFmRequests());

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmDeleteFault,
                                               filter, sizeof(*filter));
  if (erc!=FM_ERR_OK) return erc;

  FM_INFO_LOG("Enqueue clear alarm request: alarm id (%s), instant id (%s)",
              filter->alarm_id, filter->entity_instance_id);
  enqueue(buff);

  return FM_ERR_OK;
}


EFmErrorT fm_clear_all_async(fm_ent_inst_t *inst_id) {

  if ( !fm_lib_thread()) return FM_ERR_RESOURCE_UNAVAILABLE;
  CHECK_LIST_FULL(GetListOfFmRequests());

  fm_buff_t buff;
  buff.clear();
  EFmErrorT erc = fm_msg_utils_prep_requet_msg(buff, EFmDeleteFaults,
                                               (*inst_id), sizeof(*inst_id));
  if (erc!=FM_ERR_OK) return erc;

  FM_INFO_LOG("Enqueue clear all alarm request: instant id (%s)", *inst_id);
  enqueue(buff);
  return FM_ERR_OK;
}

}