//
// 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;
}

}