//
//nodeserv.c
//
//Noderef server. Functions for managing noderefs.
//
//
//-UserX 2001/11/13

#include "misc/compat.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "net/nodeserv.h"
#include "net/noderef.h"
#include "crypt/random.h"
#include "base/logger.h"
#include "base/str.h"
#include "base/strarray.h"
#include "base/parse.h"
#include "misc/global.h"

#include "net/sock.h"

#include "base/array.h"

enum NR_ {
		NR_HOST,
		NR_PORT,
		NR_PROTOCOL,
		NR_TRANSPORT,
		NR_PUBLICKEY,
		NR_PRIVATEKEY,
		NR_SPECIAL,
		NR_NETNAME,
		NR_VERSION,

		NR_NETWORK_KEY,
		NR_NETWORK_SECRET_KEY,
		NR_NETWORK_PROTOCOL,
		NR_NETWORK_NAME,
		NR_NETWORK_VERSION
	};

StringIntPair noderefkeys[] = {
		{"host", NR_HOST},
		{"port", NR_PORT},
		{"protocol", NR_PROTOCOL},
		{"transport", NR_TRANSPORT},
		{"publickey", NR_PUBLICKEY},
		{"privatekey", NR_PRIVATEKEY},
		{"special", NR_SPECIAL},
		{"netname", NR_NETNAME},
		{"version", NR_VERSION},

		{"networkid", NR_NETWORK_KEY} ,
		{"networkkey", NR_NETWORK_KEY} ,
		{"networksecret", NR_NETWORK_SECRET_KEY} ,
		{"networkprivatekey", NR_NETWORK_SECRET_KEY} ,
		{"networkentry", NR_NETWORK_PROTOCOL},
		{"networkprotocol", NR_NETWORK_PROTOCOL} ,
		{"networkname", NR_NETWORK_NAME} ,
		{"networkversion", NR_NETWORK_VERSION} ,
		{NULL, -1}
	};

StringIntPair listenrefkeys[] = {
		{"host", NR_HOST},
		{"port", NR_PORT},
		{"protocol", NR_PROTOCOL},
		{"transport", NR_TRANSPORT},
		{"publickey", NR_PUBLICKEY},
		{"privatekey", NR_PRIVATEKEY},
		{"special", NR_SPECIAL},
		{NULL, -1}
	};

char defaultnodereffilename[] = DEFAULT_NODEREFFILENAME;
char *nodereffilename = NULL;

char defaultlistenreffilename[] = "listen.ref";
char *listenreffilename = NULL;

char *networkname = NULL;
char *networkversion = NULL;

int noderefreload = 1;

int nodeservCheckNetworkNameVersion(char *netname, char * netversion) {
	StringArrayHandle *sanew;
	StringArrayHandle *sacurrent;
	int i;

	if(isStringBlank(netname) || isStringBlank(networkname)) {
		i = 1;
	} else {
		sanew = saMakeFromSplitString(netname, ",");
		sacurrent = saMakeFromSplitString(networkname, ",");
		saTrim(sanew, " \t");
		saTrim(sacurrent, " \t");
		i = saMatchAny(sanew, sacurrent);
		saFree(sanew);
		saFree(sacurrent);
	}

	if(i == 0) {
		return 0;
	}

	if(isStringBlank(netversion) || isStringBlank(networkversion)) {
		i = 1;
	} else {
		sanew = saMakeFromSplitString(netversion, ",");
		sacurrent = saMakeFromSplitString(networkversion, ",");
		saTrim(sanew, " \t");
		saTrim(sacurrent, " \t");
		i = saMatchAny(sanew, sacurrent);
		saFree(sanew);
		saFree(sacurrent);
	}

	return i;
}

void nodeservAddNodeRefFile(char *filename) {
	int i;
	char *s;

	char *netname = NULL;
	char *netversion = NULL;
	int netok = 1;

	StringPairArrayHandle *spa;
	ENodeRef *nr = NULL;

	nraCheckMake();

	spa = spaReadFile(filename);
	if(spa == NULL) {
		LOGERROR(stringCopyMany("nodeservAddNodeRefFile: no noderef file, filename(", filename, "): No 'node.ref' file found.", NULL));
		return;
	}
	LOGDEBUG(stringJoin("nodeservAddNodeRefFile: spa->size:", intToString(spa->size)));

	for(i = 0; i < spa->size; i++) {
		s = spa->data[i].val;
		switch(stringIntPairSearch(noderefkeys, spa->data[i].key)) {
		case NR_NETWORK_NAME:
			netname = stringReplace(netname, s);
			netok = nodeservCheckNetworkNameVersion(netname, netversion);
			break;
		case NR_NETWORK_VERSION:
			netversion = stringReplace(netversion, s);
			netok = nodeservCheckNetworkNameVersion(netname, netversion);
			break;
		}
		if(netok != 0) {
			switch(stringIntPairSearch(noderefkeys, spa->data[i].key)) {
			case NR_HOST:
				if(s != NULL) {
					//todo: format checking on hostname
					nr = nraAdd(s);
				}
				break;
			case NR_PORT:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					//todo: numeric format checking
					nr->noderef->PortNum = atoi(s);
				}
				break;
			case NR_PROTOCOL:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					//todo: format checking
					nr->noderef->Protocol = stringReplace(nr->noderef->Protocol, s);
				}
				break;
			case NR_TRANSPORT:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					//todo: format checking
					nr->noderef->Transport = stringReplace(nr->noderef->Transport, s);
				}
				break;
			case NR_PUBLICKEY:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					//todo: format checking
					nr->noderef->PublicKey = stringReplace(nr->noderef->PublicKey, s);
				}
				break;
			case NR_PRIVATEKEY:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					//todo: format checking
					nr->noderef->PrivateKey = stringReplace(nr->noderef->PrivateKey, s);
				}
				break;
			case NR_SPECIAL:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					//todo: format checking
					nr->noderef->Special = stringReplace(nr->noderef->Special, s);
					switch(stringIntPairSearch(noderefspecials, nr->noderef->Special)) {
					case NRS_ENTRY:
						if(nr->noderef->PublicKey == NULL && networkKey != NULL) {
							nr->noderef->PublicKey = stringCopy(networkKey);
						}
						if(nr->noderef->Protocol == NULL && networkProtocol != NULL) {
							nr->noderef->Protocol = stringCopy(networkProtocol);
						}
						break;
					case NRS_EXIT:
						if(nr->noderef->PrivateKey == NULL && networkKey != NULL) {
							nr->noderef->PrivateKey = stringCopy(networkSecretKey);
						}
						if(nr->noderef->Protocol == NULL && networkProtocol != NULL) {
							nr->noderef->Protocol = stringCopy(networkProtocol);
						}
						break;
					case -1:
					default:
						if(isStringBlank(nr->noderef->Special)) {
							LOGERROR("Unknown/unhandled special type.");
						}
					}
				}
				break;
			case NR_NETNAME:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					nr->noderef->netname = stringReplace(nr->noderef->netname, s);
				}
				break;
			case NR_VERSION:
				if(nr == NULL) {
					LOGERROR("\"host\" must be first in a node reference file.");
				} else {
					nr->noderef->version = stringReplace(nr->noderef->version, s);
				}
				break;
			case NR_NETWORK_KEY:
				networkKey = stringReplace(networkKey, s);
				break;
			case NR_NETWORK_SECRET_KEY:
				networkSecretKey = stringReplace(networkSecretKey, s);
				break;
			case NR_NETWORK_PROTOCOL:
				networkProtocol = stringReplace(networkProtocol, s);
				break;
			case -1:
				LOGERROR(stringCopyMany(
						"File: \"", 
						filename,
						"\", unknown key \"",
						spa->data[i].key,
						"\", with value \"",
						spa->data[i].val,
						"\"",
						NULL));
				break;
			case NR_NETWORK_NAME:
			case NR_NETWORK_VERSION:
				break;
			default:
				LOGERROR(stringCopyMany(
						"Internal error: Key \"", 
						spa->data[i].key,
						"\" is known but is unhandled, with value \"",
						spa->data[i].val,
						"\"",
						NULL));

			}
		}
	}

	spaFree(spa);
	stringFree(netname);
	stringFree(netversion);
}


//Sets the name of the node ref file. or reset to the default if filename is blank or NULL.
void nodeservSetNodeRefFile(char *filename) {
	
	stringFree(nodereffilename);
	if(isStringBlank(filename)) {
		nodereffilename = NULL;
	} else {
		nodereffilename = stringCopy(filename);
	}
}

void nodeservReadNodeRefFile() {
	if(nodereffilename == NULL) {
		nodeservAddNodeRefFile(defaultnodereffilename);
	} else {
		nodeservAddNodeRefFile(nodereffilename);
	}
	LOGMINOR(stringJoinMany("nodeservReadNodeRefFile: noderef total(", intToString(NRA->size),")", NULL));
}

void nodeservClearNodeRefs(void) {
	if(NRA == NULL) {
		nraCheckMake();
	}
	LOGMINOR("nodeservClearNodeRef: Cleared noderefs");
	arrayDelete((ArrayHandle *)NRA, 0, NRA->size);
}

//todo: make node selection more aware of statistics

//picks a node
NodeRef *nodeservPickNode() {
	int i;
	if(noderefreload != 0) {
		nodeservClearNodeRefs();
		nodeservReadNodeRefFile();
	}
	if(NRA == NULL || NRA->size == 0) {
		return NULL;
	}
	i = randomint(NRA->size);
	LOGMINOR(stringJoinMany("nodeservPickNode: Node index(", intToString(i),")", NULL));
	return noderefCopy(NRA->data[i].noderef);
}

//picks the next NodeRef
//The input NodeRef will be released.
NodeRef *nodeservPickNextNode(NodeRef *nr) {
	int i = nraFindNodeByITag(nr->iTag);
	if(i == -1) {
		return nodeservPickNode();
	}
	noderefFree(nr);
	return noderefCopy(NRA->data[(++i) % NRA->size].noderef);
}

int nodeservTotalNodeRefs(){
	return NRA->size;
}



void nodeservAddListenRefFile(char *filename) {
	int i;
	char *s;
	StringPairArrayHandle *spa;
	NodeRef *nr = NULL;

	nraCheckMake();

	spa = spaReadFile(filename);
	if(spa == NULL) {
		LOGERROR(stringCopyMany("nodeservAddListenRefFile: no listenref file, filename(", filename, "): No 'listen.ref' file found.", NULL));
		return;
	}
	LOGDEBUG(stringJoin("nodeservAddListenRefFile: spa->size:", intToString(spa->size)));

	for(i = 0; i < spa->size; i++) {
		s = spa->data[i].val;
		switch(stringIntPairSearch(listenrefkeys, spa->data[i].key)) {
		case NR_HOST:
			if(s != NULL) {
				//todo: format checking on hostname
				nr = lnraAdd(s);
				if(nr != NULL) {
					nr->PortNum = localPort;
				}
			}
			break;
		case NR_PORT:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				//todo: numeric format checking
				nr->PortNum = atoi(s);
			}
			break;
		case NR_PROTOCOL:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				//todo: format checking
				nr->Protocol = stringReplace(nr->Protocol, s);
			}
			break;
		case NR_TRANSPORT:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				//todo: format checking
				nr->Transport = stringReplace(nr->Transport, s);
			}
			break;
		case NR_PUBLICKEY:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				//todo: format checking
				nr->PublicKey = stringReplace(nr->PublicKey, s);
			}
			break;
		case NR_PRIVATEKEY:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				//todo: format checking
				nr->PrivateKey = stringReplace(nr->PrivateKey, s);
			}
			break;
		case NR_SPECIAL:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				//todo: format checking
				nr->Special = stringReplace(nr->Special, s);
				switch(stringIntPairSearch(noderefspecials, nr->Special)) {
				case NRS_ENTRY:
					if(nr->PublicKey == NULL && networkKey != NULL) {
						nr->PublicKey = stringCopy(networkKey);
					}
					if(nr->Protocol == NULL && networkProtocol != NULL) {
						nr->Protocol = stringCopy(networkProtocol);
					}
					break;
				case NRS_EXIT:
					if(nr->PrivateKey == NULL && networkKey != NULL) {
						nr->PrivateKey = stringCopy(networkSecretKey);
					}
					if(nr->Protocol == NULL && networkProtocol != NULL) {
						nr->Protocol = stringCopy(networkProtocol);
					}
					break;
				case -1:
				default:
					if(!isStringBlank(nr->Special)) {
						LOGERROR("Unknown/unhandled special type.");
					}
				}
			}
			break;
		case NR_NETNAME:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				nr->netname = stringReplace(nr->netname, s);
			}
			break;
		case NR_VERSION:
			if(nr == NULL) {
				LOGERROR("\"host\" must be first in a node reference file.");
			} else {
				nr->version = stringReplace(nr->version, s);
			}
			break;
		case -1:
			LOGERROR(stringCopyMany(
					"File: \"", 
					filename,
					"\", unknown key \"",
					spa->data[i].key,
					"\", with value \"",
					spa->data[i].val,
					"\"",
					NULL));
			break;
		default:
			LOGERROR(stringCopyMany(
					"Internal error: Key \"", 
					spa->data[i].key,
					"\" is know but is unhandled, with value \"",
					spa->data[i].val,
					"\"",
					NULL));

		}
	}

	spaFree(spa);
}

//Sets the name of the node ref file. or reset to the default if filename is blank or NULL.
void nodeservSetListenRefFile(char *filename) {
	
	stringFree(listenreffilename);
	if(isStringBlank(filename)) {
		listenreffilename = NULL;
	} else {
		listenreffilename = stringCopy(filename);
	}
}

void nodeservReadListenRefFile(void) {
	if(listenreffilename == NULL) {
		nodeservAddListenRefFile(defaultlistenreffilename);
	} else {
		nodeservAddListenRefFile(listenreffilename);
	}
}

void nodeservClearListenRefs(void) {
	if(LNRA == NULL) {
		nraCheckMake();
	}
	arrayDelete((ArrayHandle *)LNRA, 0, LNRA->size);
}

void ndoeservWriteListenRefFile(char *filename) {
	//FileHandle *fh;
	StringPairArrayHandle *spa;
	int i;
	NodeRef *nr = NULL;
	if(isStringBlank(filename)) {
		return;
	}
	//fh = fileOpen(filename, "w");
	spa = spaMake();
	for(i = 0; i < LNRA->size; i++) {
		nr = &LNRA->data[i];
		spaAppend(spa, stringPairMakeCopy("host", nr->HostName));
		//fprintf(fh, "host = %s\n", nr->HostName);
		if(nr->PortNum != localPort) {
			//fprintf(fh, "port = %d\n", nr->PortNum);
			spaAppend(spa, stringPairMake("port", intToString(nr->PortNum)));
		}
		if(!isStringBlank(nr->Transport)) {
			//fprintf(fh, "transport = %s\n", nr->Transport);
			spaAppend(spa, stringPairMakeCopy("transport", nr->Transport));
		}
		if(stringCaseCompare(nr->Special, "entry") == 0) {
			//fprintf(fh, "special = %s\n", nr->Special);
			spaAppend(spa, stringPairMakeCopy("special", nr->Special));
			if(!isStringBlank(nr->PublicKey)) {
				if(stringCaseCompare(nr->PublicKey, networkKey) != 0) {
					//fprintf(fh, "publickey = %s\n", nr->PublicKey);
					spaAppend(spa, stringPairMakeCopy("publickey", nr->PublicKey));
				}
			}
			if(!isStringBlank(nr->Protocol)) {
				if(stringCaseCompare(nr->Protocol, networkProtocol) != 0) {
					//fprintf(fh, "protocol = %s\n", nr->Protocol);
					spaAppend(spa, stringPairMakeCopy("protocol", nr->Protocol));
				}
			}
		} else {
			if(!isStringBlank(nr->PublicKey)) {
				//fprintf(fh, "publickey = %s\n", nr->PublicKey);
				spaAppend(spa, stringPairMakeCopy("publickey", nr->PublicKey));
			}
			if(!isStringBlank(nr->Protocol)) {
				//fprintf(fh, "protocol = %s\n", nr->Protocol);
				spaAppend(spa, stringPairMakeCopy("protocol", nr->Protocol));
			}
		}
		if(!isStringBlank(nr->PrivateKey)) {
			//fprintf(fh, "privatekey = %s\n", nr->PrivateKey);
			spaAppend(spa, stringPairMakeCopy("privatekey", nr->PrivateKey));
		}
	}
	//fclose(fh);
	spaWriteFile(spa, filename);
	spaFree(spa);
}

void nodeservWriteListenRef(void) {
	if(isStringBlank(listenreffilename)) {
		ndoeservWriteListenRefFile(defaultlistenreffilename);
	} else {
		ndoeservWriteListenRefFile(listenreffilename);
	}
}


//submits a noderef to an email address
//somewhat quick and dirty. will appear to "lock up" if the server doesn't
//reply to SMTP commands.
//returns non-zero if failed.
int nodeservSubmitNode(NodeRef *nr, char *page, char *server, int port) {
	NodeRef *enr;
	SockHandle *sh;
	char *s;
	char *l, *r;
	char *code;
	if(server == NULL || page == NULL) {
		return 1;
	}
	if(port == 0) {
		port = 80;
	}
	enr = noderefMake();
	noderefInit(enr, stringCopy(server), NULL, "tcp", NULL, NULL, NULL, NULL, NULL, port);
	sh = sockMake();
	if(sockSetNodeRef(sh, enr) != SOCK_ERR_NONE) {
		sockFree(sh);
		noderefFree(enr);
		return 1;
	}

	if(sockOpen(sh) != SOCK_ERR_NONE) {
		sockFree(sh);
		noderefFree(enr);
		return 1;
	}

	s = stringCopyMany(
			page, "?",
			"clientversion=110&", // this should be read from a global constant
			"host=", nr->HostName, "&",
			"port=", intToString(nr->PortNum), "&",
			"protocol=", nr->Protocol, "&",
			"publickey=", nr->PublicKey, "&",
			"networkname=", networkname, "&",
			"networkversion=", networkversion, "&",
			NULL);
	pipeWriteString(&sh->IOPipe, stringCopyMany(
			"GET ", s, " HTTP/1.0\x0d\x0a",
			"User-Agent: iip/1.1\x0d\x0a",
			"Accept: */*\x0d\x0a",
			"\x0d\x0a",
			NULL
		));
	//stringFree(s);

	while((sh->IOPipe.outBuffer->size != 0) && ((sh->status & SOCK_STATUS_CLOSING) == 0)) {
		sockWrite(sh);
	}
	while((sh->status & SOCK_STATUS_CLOSING) == 0) {
		sockRead(sh);
	}

	s = dblockToString(sh->IOPipe.inBuffer);
	sockClose(sh);
	sockFree(sh);
	noderefFree(enr);
	
	r = stringSplit(s, "\x0d\x0a");
	l = stringSplit(r, " ");
	code = stringSplit(r, " ");
	code = stringTrim(code, " ");
	stringFree(l);
	stringFree(r);
	l = stringSplit(s, "\x0d\x0a\x0d\x0a");
	stringFree(l);
	if(stringCompare(code, "200") == 0) {
		stringFree(s);
		return 0;
	} else {
		stringFree(s);
		return 1;
	}
}


//Qucik and drity HTTP page request
//todo: maybe: add proxy support to this
int nodeservGetNodeRefs(char *page, char *server, int port) {
	FileHandle *fh;
	NodeRef *enr;
	SockHandle *sh;
	char *s;
	char *l, *r;
	char *code;
	if(server == NULL || page == NULL) {
		return 1;
	}
	if(port == 0) {
		port = 80;
	}
	enr = noderefMake();
	noderefInit(enr, stringCopy(server), NULL, "tcp", NULL, NULL, NULL, NULL, NULL, port);
	sh = sockMake();
	if(sockSetNodeRef(sh, enr) != SOCK_ERR_NONE) {
		sockFree(sh);
		noderefFree(enr);
		return 1;
	}

	if(sockOpen(sh) != SOCK_ERR_NONE) {
		sockFree(sh);
		noderefFree(enr);
		return 1;
	}

	s = stringAppend2(server, stringAppend2(":", intToString(port)));
	pipeWriteString(&sh->IOPipe, stringCopyMany(
			"GET ", page, " HTTP/1.0\x0d\x0a",
			"User-Agent: iip/1.1\x0d\x0a",
			"Host:", s, "\x0d\x0a",
			"Accept: */*\x0d\x0a",
			"\x0d\x0a",
	//		"\x0d\x0a",
			NULL
		));
	stringFree(s);

	while((sh->IOPipe.outBuffer->size != 0) && ((sh->status & SOCK_STATUS_CLOSING) == 0)) {
		sockWrite(sh);
	}
	while((sh->status & SOCK_STATUS_CLOSING) == 0) {
		sockRead(sh);
	}

	s = dblockToString(sh->IOPipe.inBuffer);
	sockClose(sh);
	sockFree(sh);
	noderefFree(enr);
	
	r = stringSplit(s, "\x0d\x0a");
	l = stringSplit(r, " ");
	code = stringSplit(r, " ");
	code = stringTrim(code, " ");
	stringFree(l);
	stringFree(r);
	l = stringSplit(s, "\x0d\x0a\x0d\x0a");
	stringFree(l);
	if(stringCompare(code, "200") == 0 && nodeservNodeRefListValid(s)) {
		if(nodereffilename == NULL) {
			fh = fileOpen(defaultnodereffilename, "w");
		} else {
			fh = fileOpen(nodereffilename, "w");
		}
		if(fh != NULL) {
			//todo: error checking
			fileWriteStringKeep(fh, s);
			fileClose(fh);
		}
		nodeservClearNodeRefs();
		nodeservReadNodeRefFile();
		stringFree(s);
		return 0;
	} else {
		stringFree(s);
		return 1;
	}
}


int nodeservNodeRefListValid(char *nodereflist) {
	/*todo: make this smarter*/
	if(
		strstr(nodereflist, "host") != NULL ||
		strstr(nodereflist, "Host") != NULL ||
		strstr(nodereflist, "HOST") != NULL ||
		strstr(nodereflist, "hOST") != NULL
	) {
		return 1;
	} else {
		return 0;
	}
}

//todo: node statistic functions
