#include <pthread.h>
#include <assert.h>
#include <getopt.h>
#include "common.h"
#include <signal.h>
#include <unistd.h>
#include <openssl/err.h>
#include <stdbool.h>
#include "cJSON.h"

#ifdef DEBUG
#define DEBUG_PRINT(...) do{ fprintf( stdout, __VA_ARGS__ ); } while( false )
#else
#define DEBUG_PRINT(...) do{ } while ( false )
#endif

const char *USAGE="client [-v | -V logfile ] [ -k keylog ] [-c context_list] [-s server] [-m middlebox_list | -M middlebox_list_file]\n"
	"\tcontext list must be comma-separated list of context names, defaults to \"header,body\"\n"
	"\tserver must be url including port, default 127.0.0.1:" xstr(DEFAULT_SERVER_PORT) "\n"
	"\tmiddlebox list must semi-colon delimited urls including port,"
	"\tmiddlebox list_file must be a file with middlebox urls on separate lines,"
	"\t\tdefault \"127.0.0.1:" xstr(DEFAULT_MBOX_PORT) "\n"
	"Input to send to the server is taken from stdin in lines of the form context:command\n";

// TO-DO: Accept input file as command line argument

const char *ARG_TYPES="v:V:k:c:s:m:M";

typedef struct ClientConfigST 
{
	FILE *vlog;
	FILE *fIn;
	FILE *fOut;
	SSL *pSSL;
	int numContexts;
	const char *contextDescs[50];
	int numMiddleboxes;
	const char *MiddleboxURLs[50];
	int middleboxReadRights[50][50];		// This is horrible
	int middleboxWriteRights[50][50];	 	// This is horrible and has a silly name
	const char *serverURL;
	char myKeyFile[1024];
	char myPassword[1024];
	char caFile[1024];
} ClientConfig;

static int
client_ParseArgs(int iArgC, char *papsArgV[], ClientConfig *ptCConf);

void
client_SetDefaults(ClientConfig *ptC)
{
	memset(ptC,0,sizeof(*ptC));
	ptC->fIn=stdin;
	ptC->fOut=stdout;
	strcpy(ptC->caFile,"ca.pem");
	strcpy(ptC->myKeyFile,"client.pem");
	strcpy(ptC->myPassword,"password");
	ptC->serverURL="127.0.0.1:" xstr(DEFAULT_SERVER_PORT);
}

extern char *optarg;
//the next two are available but currently ununsed,
//commented out to prevent compiler complaining
//extern int optind;
//extern int optopt;
static int
client_ParseArgs(int iArgC, char *papsArgV[], ClientConfig *ptCConf)
{
	int iStatus=0;
	char carg=0;

	while( iStatus==0 && (carg=getopt(iArgC,papsArgV,ARG_TYPES)) != -1) {
		switch(carg) {
		case 'v':
		case 'V':
			if(ptCConf->vlog!=NULL) {
				COMMON_LogErrorAndExit(iStatus=INVALID_COMMAND_LINE_ARGS,stderr,"verbose logging specified multiple times\n%s",USAGE);
			}
			if(carg=='v') {
				ptCConf->vlog=stdout;
			} else { /*carg=='V'*/
				ptCConf->vlog=fopen(optarg,"w");
				if(ptCConf->vlog==NULL) {
					COMMON_LogErrorAndExit(iStatus=FILE_NOT_WRITABLE,stderr,"Unable to open log file %s",optarg);
				}
			}
			break;
		case 's':
			/*todo*/
			assert(0);
			break;
		case 'k':
			/*todo*/
			assert(0);
			break;
		case 'c':
			/*todo*/
			assert(0);
			break;
		case 'm':
			/*todo*/
			assert(0);
			break;
		case 'M':
			/*todo*/
			assert(0);
			break;
		default:
			COMMON_LogErrorAndExit(iStatus=INVALID_COMMAND_LINE_ARGS,stderr,"Unrecognised command line option -%c",carg);
			break; /*never reached - keeps compiler happy*/
		}
	}

	return iStatus;
}

ERROR_STATUS
client_CreateContext(SSL_CTX **pptSSLctx,const ClientConfig *ptConf)
{
	ERROR_STATUS iStatus;
	iStatus=COMMON_InitializeSSLCtx(pptSSLctx,
			ptConf->myKeyFile, ptConf->myPassword,
			ptConf->caFile,
			ID_CLIENT);
	return iStatus;

}

ERROR_STATUS
client_Connect(SSL_CTX *ptSSLctx, ClientConfig *ptConf)
{
	ERROR_STATUS iStatus=SUCCESS;
	int i,j,p;
	BIO *ptSBio;

	/*step 0 - create SSL structure*/
	/*the SSL_CTX already has out keys set up*/
	ptConf->pSSL=SSL_new(ptSSLctx);
	/*todo - check for NULL return*/
	assert(ptConf->pSSL!=NULL);

	/*step 1 - create the contexts*/
	iStatus=COMMON_InitMulticontextSet(ptConf->pSSL);
	assert(iStatus==SUCCESS);
	
	if(ptConf->numContexts==0) 
	{
		DEBUG_PRINT("[CLIENT DEBUG] No contexts specified, adding default\n");
		iStatus=COMMON_AppendContext(ptConf->pSSL,"default");
		assert(iStatus==SUCCESS);
	} 
	else 
	{
		DEBUG_PRINT("[CLIENT DEBUG] Appending %d context\n", ptConf->numContexts);
		/*Iterate through the context descriptions and create each one*/
		for(i=0;i!=ptConf->numContexts && iStatus==SUCCESS;i++) 
		{
			DEBUG_PRINT("    [CLIENT DEBUG] Appending context %s\n", ptConf->contextDescs[i]);
			iStatus=COMMON_AppendContext(ptConf->pSSL,ptConf->contextDescs[i]);
			assert(iStatus==SUCCESS);
		}

	}
	if(iStatus!=SUCCESS) 
	{
		/*TODO*/
		assert(0);
	}

	/*step 2 - create the middlebox list*/
	iStatus=COMMON_InitProxySet(ptConf->pSSL);
	assert(iStatus==SUCCESS);
	/*step 2.0? - do we need to insert client as middlebox 0? - TODO confirm*/
	/*step 2.1 - iterate through middlebox list */
	DEBUG_PRINT("[CLIENT DEBUG] Appending %d middleboxes\n", ptConf->numMiddleboxes );
	for(i=0;i!=ptConf->numMiddleboxes;i++) 
	{
		DEBUG_PRINT("    [CLIENT DEBUG] Appending middlbox %d (%s)\n", i, ptConf->MiddleboxURLs[i]);
		iStatus=COMMON_AppendProxy(ptConf->pSSL,ptConf->MiddleboxURLs[i]);
	}
	/*step 2.2 - add server url*/

	DEBUG_PRINT("[CLIENT DEBUG] Setting server URL { %s }\n", ptConf->serverURL);
	iStatus=COMMON_SetServer(ptConf->pSSL,ptConf->serverURL);

	DEBUG_PRINT("[CLIENT DEBUG] Setting middlebox access permissions\n");
	for(j=0;j!=ptConf->numContexts;j++) 
	{
		/*grant client if included*/
		/*grant server*/
		DEBUG_PRINT("    [CLIENT DEBUG] Setting permissions for slice %d (%s)\n", ptConf->pSSL->slices[j]->slice_id,ptConf->pSSL->slices[j]->purpose);
		//DEBUG_PRINT("        [CLIENT DEBUG] Setting full permissions for server (%d)\n", ptConf->numMiddleboxes-1);
		//iStatus=COMMON_SetProxyAccessPermissionByID(ptConf->pSSL,ptConf->pSSL->slices[j]->slice_id, ptConf->numMiddleboxes-1,
		//		1,1);
		for(i=0;i!=ptConf->numMiddleboxes/*-1*/;i++) 
		{ /*change to i=1 if client included*/
			char *sMBoxUrl=ptConf->pSSL->proxies[i]->address;
			char *sPort=strchr(sMBoxUrl,':');
			int writePermission = ptConf->middleboxWriteRights[i][j];
			int readPermission = ptConf->middleboxReadRights[i][j];
			assert(sPort);
			sPort++;
			int iPort=atoi(sPort);
			DEBUG_PRINT("        [CLIENT DEBUG] Setting permissions for middlebox %d (%s) R = %d W = %d\n", i, sMBoxUrl, readPermission, writePermission);
			iStatus=COMMON_SetProxyAccessPermissionByID(ptConf->pSSL,ptConf->pSSL->slices[j]->slice_id, i,
					readPermission, writePermission);
		}
	}

	#ifdef DEBUG
	printf ("----------------------------\n");
	for (int q = 0; q < ptConf->pSSL->proxies_len; q++)
	{
		printf ("Proxy %d: \n", q);
		printf ("  WriteSliceIDs: \n");
		for (int p = 0; p < ptConf->pSSL->proxies[0]->write_slice_ids_len; p++)
		{
			printf ("     [%d]: %d\n", p, ptConf->pSSL->proxies[0]->write_slice_ids[p]);
		}
		printf ("  ReadSliceISs: \n");
		for (int p = 0; p < ptConf->pSSL->proxies[0]->read_slice_ids_len; p++)
		{
			printf ("     [%d]: %d\n", p, ptConf->pSSL->proxies[0]->read_slice_ids[p]);
		}
	}
	printf ("----------------------------\n");
	#endif

	/*step 4? - do we need to create a socket to first middlebox?*/
	// TCP Connect
	char* sAddress = (char*)malloc(strlen(ptConf->pSSL->proxies[0]->address) + 1); // Large enough for string+\0
	memcpy(sAddress, ptConf->pSSL->proxies[0]->address, strlen(ptConf->pSSL->proxies[0]->address) + 1);
	char *sHost = strtok(sAddress, ":");
	int iPort = atoi(strtok(NULL, ":"));
	int iSock;
	DEBUG_PRINT ("[CLIENT DEBUG] Connecting to %s\n", sHost);
	COMMON_TcpConnect(&iSock, sHost, iPort);
	// Connect TCP socket to SSL socket
	ptSBio = BIO_new_socket(iSock, BIO_NOCLOSE);
    SSL_set_bio(ptConf->pSSL, ptSBio, ptSBio);

	/*step 5 - call the connect method*/
	/*SPP connect calls this method but also adds some checks,
	 * in particular it checks that there is at least one context (fine)
	 * but also checks that there is at least one proxy (which seems
	 * unnecassary but the server is included so there must be one)
	 */
	if (SSL_connect(ptConf->pSSL) <= 0) {
		iStatus=NETWORK_CONNECT_FAIL;
	}

	X509* client_cert = SSL_get_certificate(ptConf->pSSL);
        printf ("[CLIENT] Client certificate:\n");
        COMMON_PrintCertificateDetails(client_cert);
        X509_free(client_cert);

        X509* server_cert = SSL_get_peer_certificate(ptConf->pSSL);
        printf ("[CLIENT] Server certificate:\n");
        COMMON_PrintCertificateDetails(server_cert);
        X509_free(server_cert);
	
	return SUCCESS;
	// MC: Do we really want to return SUCCESS regardless of the value of iStatus?
}

int intCount = 0;

struct iobuffer {
	int iUsed;
	union {
		unsigned char aucBuffer[1024];
		char stext[1024];
	};
	char z;/*force null termination of string*/
};

int main(const int iArgC, char *papsArgV[])
{
	DEBUG_PRINT ("[CLIENT DEBUG] Client starting\n");

	ClientConfig tConfig;
	SSL_CTX *ptSSLctx=NULL;
	int iStatus;

	client_SetDefaults(&tConfig);

	char* inputFile = "inputData.json";
	DEBUG_PRINT ("[CLIENT DEBUG] Reading input data from %s\n", inputFile);
	cJSON* json = COMMON_ReadJSONFile(inputFile);
	cJSON* item = NULL;

	char* configStr = cJSON_Print(json);
	DEBUG_PRINT("%s\n", configStr);
	free(configStr);

	// TO DO bounds, null or error checking of any kind at all
	cJSON* contextNames = cJSON_GetObjectItem(json, "contexts");
	int contextCount = 0;
	cJSON_ArrayForEach(item, contextNames)
	{
		tConfig.contextDescs[contextCount] = strdup(item->valuestring);
		contextCount++;
	}
	tConfig.numContexts = contextCount;
	
	
	cJSON* middleboxes = cJSON_GetObjectItem(json, "middleboxes");
	int mboxCount = 0;
	cJSON_ArrayForEach(item, middleboxes)
	{
		tConfig.MiddleboxURLs[mboxCount] = strdup(cJSON_GetObjectItem(item, "url")->valuestring);
		cJSON* readAccessItem = NULL;
		cJSON_ArrayForEach(readAccessItem, cJSON_GetObjectItem(item, "readAccess"))
		{
			tConfig.middleboxReadRights[mboxCount][readAccessItem->valueint] = 1;
		}
		cJSON* writeAccessItem = NULL;
		cJSON_ArrayForEach(writeAccessItem, cJSON_GetObjectItem(item, "writeAccess"))
		{
			tConfig.middleboxWriteRights[mboxCount][writeAccessItem->valueint] = 1;
		}
		mboxCount++;
	}
	tConfig.numMiddleboxes = mboxCount;
	
	iStatus=client_ParseArgs(iArgC,papsArgV, &tConfig);
	COMMON_CheckLogErrorAndExit(iStatus,stderr,"%s\n",USAGE);

	iStatus=client_CreateContext(&ptSSLctx,&tConfig);
	iStatus=client_Connect(ptSSLctx,&tConfig);

	DEBUG_PRINT("[CLIENT DEBUG] Connected\n");

	struct iobuffer tInBuff;

	cJSON* slicedData = cJSON_GetObjectItem(json, "slicedData");
	cJSON_ArrayForEach(item, slicedData)
    {
        cJSON *slice = cJSON_GetObjectItem(item, "slice");
        cJSON *text = cJSON_GetObjectItem(item, "data");
		if (slice->valueint >= tConfig.numContexts)
		{
			printf("[CLIENT ERROR] Data specified for slice id %d, which is out of bounds\n", slice->valueint);
			exit(-1);
		}

		tConfig.pSSL->write_slice=SPP_get_slice_by_id(tConfig.pSSL,tConfig.pSSL->slices[slice->valueint]->slice_id);
		if (tConfig.pSSL->write_slice == NULL)
		{
			printf("[CLIENT ERROR] Data specified for slice id %d which is not found\n", slice->valueint);
			exit(-1);
		}
		int iBytesSent=SSL_write(tConfig.pSSL, text->valuestring, strlen(text->valuestring));
		printf ("[CLIENT] Sent %d bytes { %s }\n", iBytesSent, text->valuestring);				
	}
	cJSON_Delete(json);


	int iBytesRead = 1;

	cJSON* recvData = cJSON_CreateObject();
	cJSON* sliceDataArray = cJSON_AddArrayToObject(recvData, "slices");

	SPP_SLICE* slice;
	SPP_CTX* ctx;
	int slicesSeen = 0;

	while ((iBytesRead > 0) && (slicesSeen < tConfig.numContexts))
	{
		DEBUG_PRINT ("[CLIENT DEBUG] Waiting for response\n");
		//iBytesRead = SSL_read(tConfig.pSSL,tInBuff.aucBuffer,sizeof(tInBuff.aucBuffer));
		iBytesRead = SPP_read_record(tConfig.pSSL, tInBuff.aucBuffer, sizeof(tInBuff.aucBuffer), &slice, &ctx);

		if (iBytesRead > 0)
		{
			cJSON* jslice = cJSON_CreateObject();
			cJSON_AddNumberToObject(jslice, "slice", slice->slice_id );
			cJSON_AddStringToObject(jslice, "data", COMMON_MakeNullTerminatedCopy(tInBuff.aucBuffer, iBytesRead));
			cJSON_AddItemToArray(sliceDataArray, jslice);
			DEBUG_PRINT ("[CLIENT DEBUG] Recv %d bytes [(%s)]\n", iBytesRead, tInBuff.aucBuffer);
		}
		else
		{
			DEBUG_PRINT ("[CLIENT DEBUG] SPP_read_record return zero bytes\n");
			break;
		}

		slicesSeen++;
	}

	char* filename = COMMON_WriteJSONFile(recvData, "Client");
	DEBUG_PRINT("[CLIENT DEBUG] Data written to %s\n", filename);
	free(filename);

	char* recvDataString = cJSON_Print(recvData);
	printf("[CLIENT] Received\n%s\n", recvDataString);
	free(recvDataString);

	cJSON_Delete(recvData);

	DEBUG_PRINT ("[CLIENT DEBUG] Exiting\n");
	return iStatus;
}
