
#include "stratum.h"

//#define CLIENT_DEBUGLOG_

bool client_suggest_difficulty(YAAMP_CLIENT *client, json_value *json_params)
{
	if(json_params->u.array.length>0)
	{
		double diff = client_normalize_difficulty(json_params->u.array.values[0]->u.dbl);
		uint64_t user_target = diff_to_target(diff);

		if(user_target >= YAAMP_MINDIFF && user_target <= YAAMP_MAXDIFF)
			client->difficulty_actual = diff;
	}

	client_send_result(client, "true");
	return true;
}

bool client_suggest_target(YAAMP_CLIENT *client, json_value *json_params)
{
	client_send_result(client, "true");
	return true;
}

bool client_subscribe(YAAMP_CLIENT *client, json_value *json_params)
{
	//if(client_find_my_ip(client->sock->ip)) return false;
	get_next_extraonce1(client->extranonce1_default);

	client->extranonce2size_default = YAAMP_EXTRANONCE2_SIZE;
	client->difficulty_actual = g_stratum_difficulty;

	strcpy(client->extranonce1, client->extranonce1_default);
	client->extranonce2size = client->extranonce2size_default;

	// decred uses an extradata field in block header, 2 first uint32 are set by the miner
	if (g_current_algo->name && !strcmp(g_current_algo->name,"decred")) {
		memset(client->extranonce1, '0', sizeof(client->extranonce1));
		memcpy(&client->extranonce1[16], client->extranonce1_default, YAAMP_EXTRANONCE2_SIZE*2);
		client->extranonce1[24] = '\0';
		client->extranonce2size = client->extranonce2size_default = 12;
	}

	get_random_key(client->notify_id);

	if(json_params->u.array.length>0)
	{
		strncpy(client->version, json_params->u.array.values[0]->u.string.ptr, 1023);
	//	if(!strcmp(client->version, "stratum-proxy/0.0.1")) return false;

		if(strstr(client->version, "NiceHash") || strstr(client->version, "proxy") || strstr(client->version, "/3."))
			client->reconnectable = false;

		if(strstr(client->version, "ccminer")) client->stats = true;
		if(strstr(client->version, "cpuminer-multi")) client->stats = true;
		if(strstr(client->version, "cpuminer-opt")) client->stats = true;
	}

	if(json_params->u.array.length>1)
	{
		char notify_id[1024] = { 0 };
		if (json_params->u.array.values[1]->u.string.ptr)
			strncpy(notify_id, json_params->u.array.values[1]->u.string.ptr, 1023);

		YAAMP_CLIENT *client1 = client_find_notify_id(notify_id, true);
		if(client1)
		{
			strncpy(client->notify_id, notify_id, 1023);

			client->jobid_locked = client1->jobid_locked;
//			client->jobid_next = client1->jobid_next;
			client->difficulty_actual = client1->difficulty_actual;

			client->extranonce2size_default = client1->extranonce2size_default;
			strcpy(client->extranonce1_default, client1->extranonce1_default);

			client->extranonce2size = client1->extranonce2size_reconnect;
			strcpy(client->extranonce1, client1->extranonce1_reconnect);

			client->speed = client1->speed;
			client->extranonce1_id = client1->extranonce1_id;

			client->userid = client1->userid;
			client->workerid = client1->workerid;

			memcpy(client->job_history, client1->job_history, sizeof(client->job_history));
			client1->lock_count = 0;

#ifdef CLIENT_DEBUGLOG_
			debuglog("reconnecting client locked to %x\n", client->jobid_next);
#endif
		}

		else
		{
			YAAMP_CLIENT *client1 = client_find_notify_id(notify_id, false);
			if(client1)
			{
				strncpy(client->notify_id, notify_id, 1023);

				client->difficulty_actual = client1->difficulty_actual;
				client->speed = client1->speed;

				memcpy(client->job_history, client1->job_history, sizeof(client->job_history));
				client1->lock_count = 0;

#ifdef CLIENT_DEBUGLOG_
				debuglog("reconnecting2 client\n");
#endif
			}
		}
	}

	strcpy(client->extranonce1_last, client->extranonce1);
	client->extranonce2size_last = client->extranonce2size;

#ifdef CLIENT_DEBUGLOG_
	debuglog("new client with nonce %s\n", client->extranonce1);
#endif

	client_send_result(client, "[[[\"mining.set_difficulty\",\"%.3g\"],[\"mining.notify\",\"%s\"]],\"%s\",%d]",
		client->difficulty_actual, client->notify_id, client->extranonce1, client->extranonce2size);

	return true;
}

///////////////////////////////////////////////////////////////////////////////////////////
bool client_validate_user_address(YAAMP_CLIENT *client)
{
	int client_workers = 0;
	if (client->userid == 0) {
		client_workers = client_workers_byaddress(client->username);
	} else {
		client_workers = client_workers_count(client);
	}

	// if already logged in this instance, reuse data from other workers (in memory)
	if (client_workers > 1) {
		if (client_workers > 100 && (client_workers%100 == 0)) {
			clientlog(client, "using %d workers", client_workers);
		}
		if (client_auth_by_workers(client)) {
			// client->coinid filled
			return true;
		}
	}

	if (!client->coinid) {
		for(CLI li = g_list_coind.first; li; li = li->next) {
			YAAMP_COIND *coind = (YAAMP_COIND *)li->data;
			// debuglog("user %s testing on coin %s ...\n", client->username, coind->symbol);
			if(!coind_can_mine(coind)) continue;
			if(strlen(g_current_algo->name) && strcmp(g_current_algo->name, coind->algo)) continue;
			if(coind_validate_user_address(coind, client->username)) {
				debuglog("new user %s for coin %s\n", client->username, coind->symbol);
				client->coinid = coind->id;
				// update the db now to prevent addresses conflicts
				CommonLock(&g_db_mutex);
				db_init_user_coinid(g_db, client);
				CommonUnlock(&g_db_mutex);
				return true;
			}
		}
	}

	if (!client->coinid) {
		return false;
	}

	YAAMP_COIND *coind = (YAAMP_COIND *)object_find(&g_list_coind, client->coinid);
	if (!coind) {
		clientlog(client, "unable to find the wallet for coinid %d...", client->coinid);
		return false;
	} else {
		if(g_current_algo && strlen(g_current_algo->name) && strcmp(g_current_algo->name, coind->algo)) {
			clientlog(client, "%s address is on the wrong coin %s, reset to auto...", client->username, coind->symbol);
			client->coinid = 0;
			CommonLock(&g_db_mutex);
			db_init_user_coinid(g_db, client);
			CommonUnlock(&g_db_mutex);
			return false;
		}
	}

	bool isvalid = coind_validate_user_address(coind, client->username);
	if (isvalid) {
		client->coinid = coind->id;
	} else {
		clientlog(client, "unable to verify %s address for user coinid %d...", coind->symbol, client->coinid);
	}
	return isvalid;
}

///////////////////////////////////////////////////////////////////////////////////////////

bool client_authorize(YAAMP_CLIENT *client, json_value *json_params)
{

	if(g_list_client.Find(client)) {
		clientlog(client, "Already logged");
		client_send_error(client, 21, "Already logged");
		return false;
	}

	if(json_params->u.array.length>1)
		strncpy(client->password, json_params->u.array.values[1]->u.string.ptr, 1023);

	if (g_list_client.count >= g_stratum_max_cons) {
		client_send_error(client, 21, "Server full");
		return false;
	}

	if(json_params->u.array.length>0)
	{
		strncpy(client->username, json_params->u.array.values[0]->u.string.ptr, 1023);

		db_check_user_input(client->username);
		int len = strlen(client->username);
		if (!len)
			return false;

		char *sep = strpbrk(client->username, ".,;:");
		if (sep) {
			*sep = '\0';
			strncpy(client->worker, sep+1, 1023-len);
			if (strlen(client->username) > MAX_ADDRESS_LEN) return false;
		} else if (len > MAX_ADDRESS_LEN) {
			return false;
		}
	}

	bool reset = client_initialize_multialgo(client);
	if(reset) return false;

	client_initialize_difficulty(client);

#ifdef CLIENT_DEBUGLOG_
	debuglog("new client %s, %s, %s\n", client->username, client->password, client->version);
#endif

	if(!client->userid || !client->workerid)
	{
		CommonLock(&g_db_mutex);
		db_add_user(g_db, client);

		if(client->userid == -1)
		{
			CommonUnlock(&g_db_mutex);
			client_block_ip(client, "account locked");
			clientlog(client, "account locked");

			return false;
		}

		db_add_worker(g_db, client);
		CommonUnlock(&g_db_mutex);
	}

	// when auto exchange is disabled, only authorize good wallet address...
	if (!g_autoexchange && !client_validate_user_address(client)) {

		clientlog(client, "bad mining address %s", client->username);
		client_send_result(client, "false");

		CommonLock(&g_db_mutex);
		db_clear_worker(g_db, client);
		CommonUnlock(&g_db_mutex);

		return false;
	}

	client_send_result(client, "true");
	client_send_difficulty(client, client->difficulty_actual);

	if(client->jobid_locked)
		job_send_jobid(client, client->jobid_locked);
	else
		job_send_last(client);

	g_list_client.AddTail(client);
	return true;
}

///////////////////////////////////////////////////////////////////////////////////////////

bool client_update_block(YAAMP_CLIENT *client, json_value *json_params)
{
	// password, id, block hash
	if(json_params->u.array.length < 3)
	{
		clientlog(client, "update block, bad params");
		return false;
	}

	if(strcmp(g_tcp_password, json_params->u.array.values[0]->u.string.ptr))
	{
		clientlog(client, "update block, bad password");
		return false;
	}

	YAAMP_COIND *coind = (YAAMP_COIND *)object_find(&g_list_coind, json_params->u.array.values[1]->u.integer, true);
	if(!coind) return false;

	const char* hash = json_params->u.array.values[2]->u.string.ptr;

#ifdef CLIENT_DEBUGLOG_
	debuglog("notify: new %s block %s\n", coind->symbol, hash);
#endif

	coind->newblock = true;
	coind->notreportingcounter = 0;

	if (!strcmp("DCR", coind->rpcencoding)) {
		usleep(300*YAAMP_MS);
	}

	block_confirm(coind->id, hash);

	coind_create_job(coind);
	object_unlock(coind);

	if(coind->isaux) for(CLI li = g_list_coind.first; li; li = li->next)
	{
		YAAMP_COIND *coind = (YAAMP_COIND *)li->data;
		if(!coind_can_mine(coind)) continue;
		if(coind->pos) continue;

		coind_create_job(coind);
	}

	job_signal();
	return true;
}

///////////////////////////////////////////////////////////////////////////////////////////

bool client_ask_stats(YAAMP_CLIENT *client)
{
	int id;
	if (!client->stats) return false;
	id = client_ask(client, "client.get_stats", "[]");
	return true;
}

static bool client_store_stats(YAAMP_CLIENT *client, json_value *result)
{
	if (json_typeof(result) != json_object)
		return false;

	json_value *val = json_get_val(result, "type");
	if (val && json_is_string(val)) {
		debuglog("received stats of type %s\n", json_string_value(val));
		//if (!strcmp("gpu", json_string_value(val))) {
			CommonLock(&g_db_mutex);
			db_store_stats(g_db, client, result);
			CommonUnlock(&g_db_mutex);
		//}
		return true;
	}

	return false;
}

///////////////////////////////////////////////////////////////////////////////////////////

int client_workers_count(YAAMP_CLIENT *client)
{
	int count = 0;
	if (!client || client->userid <= 0)
		return count;

	g_list_client.Enter();
	for(CLI li = g_list_client.first; li; li = li->next)
	{
		YAAMP_CLIENT *cli = (YAAMP_CLIENT *)li->data;
		if (cli->deleted) continue;
		if (cli->userid == client->userid) count++;
	}
	g_list_client.Leave();

	return count;
}

int client_workers_byaddress(const char *username)
{
	int count = 0;
	if (!username || !strlen(username))
		return count;

	g_list_client.Enter();
	for(CLI li = g_list_client.first; li; li = li->next)
	{
		YAAMP_CLIENT *cli = (YAAMP_CLIENT *)li->data;
		if (cli->deleted) continue;
		if (strcmp(cli->username, username) == 0) count++;
	}
	g_list_client.Leave();

	return count;
}

bool client_auth_by_workers(YAAMP_CLIENT *client)
{
	if (!client || client->userid < 0)
		return false;

	g_list_client.Enter();
	for(CLI li = g_list_client.first; li; li = li->next)
	{
		YAAMP_CLIENT *cli = (YAAMP_CLIENT *)li->data;
		if (cli->deleted) continue;
		if (client->userid) {
			if(cli->userid == client->userid) {
				client->coinid = cli->coinid;
				break;
			}
		} else if (strcmp(cli->username, client->username) == 0) {
			client->coinid = cli->coinid;
			client->userid = cli->userid;
			break;
		}
	}
	g_list_client.Leave();

	return (client->coinid > 0 && client->userid > 0);
}

///////////////////////////////////////////////////////////////////////////////////////////

//YAAMP_SOURCE *source_init(YAAMP_CLIENT *client)
//{
//	YAAMP_SOURCE *source = NULL;
//	g_list_source.Enter();
//
//	for(CLI li = g_list_source.first; li; li = li->next)
//	{
//		YAAMP_SOURCE *source1 = (YAAMP_SOURCE *)li->data;
//		if(!strcmp(source1->ip, client->sock->ip))
//		{
//			source = source1;
//			break;
//		}
//	}
//
//	if(!source)
//	{
//		source = new YAAMP_SOURCE;
//		memset(source, 0, sizeof(YAAMP_SOURCE));
//
//		strncpy(source->ip, client->sock->ip, 64);
//		source->speed = 1;
//
//		g_list_source.AddTail(source);
//	}
//
//	source->count++;
//
//	g_list_source.Leave();
//	return source;
//}
//
//void source_close(YAAMP_SOURCE *source)
//{
//	g_list_source.Enter();
//	source->count--;
//
//	if(source->count <= 0)
//	{
//		g_list_source.Delete(source);
//		delete source;
//	}
//
//	g_list_source.Leave();
//}
//
//void source_prune()
//{
////	debuglog("source_prune() %d\n", g_list_source.count);
//	g_list_source.Enter();
//	for(CLI li = g_list_source.first; li; li = li->next)
//	{
//		YAAMP_SOURCE *source = (YAAMP_SOURCE *)li->data;
//		source->speed *= 0.8;
//
//		double idx = source->speed/source->count;
//		if(idx < 0.0005)
//		{
//			stratumlog("disconnect all ip %s, %s, count %d, %f, %f\n", source->ip, g_current_algo->name, source->count, source->speed, idx);
//			for(CLI li = g_list_client.first; li; li = li->next)
//			{
//				YAAMP_CLIENT *client = (YAAMP_CLIENT *)li->data;
//				if(client->deleted) continue;
//				if(!client->workerid) continue;
//
//				if(!strcmp(source->ip, client->sock->ip))
//					shutdown(client->sock->sock, SHUT_RDWR);
//			}
//		}
//
//		else if(source->count > 500)
//			stratumlog("over 500 ip %s, %s, %d, %f, %f\n", source->ip, g_current_algo->name, source->count, source->speed, idx);
//	}
//
//	g_list_source.Leave();
//}

///////////////////////////////////////////////////////////////////////////////////////////

void *client_thread(void *p)
{
	YAAMP_CLIENT *client = new YAAMP_CLIENT;
	if(!client) {
		stratumlog("client_thread OOM");
		pthread_exit(NULL);
		return NULL;
	}
	memset(client, 0, sizeof(YAAMP_CLIENT));

	client->reconnectable = true;
	client->speed = 1;
	client->created = time(NULL);
	client->last_best = time(NULL);

	client->sock = socket_initialize((int)(long)p);
//	client->source = source_init(client);

	client->shares_per_minute = YAAMP_SHAREPERSEC;
	client->last_submit_time = current_timestamp();

//	usleep(g_list_client.count * 5000);

	while(!g_exiting)
	{
		if(client->submit_bad > 1024)
		{
			clientlog(client, "bad submits");
			break;
		}

		json_value *json = socket_nextjson(client->sock, client);
		if(!json)
		{
//			clientlog(client, "bad json");
			break;
		}

		client->id_int = json_get_int(json, "id");
		client->id_str = json_get_string(json, "id");

		const char *method = json_get_string(json, "method");

		if (!method && client->stats && client->id_int == client->reqid)
		{
			json_value *result = json_get_object(json, "result");
			if (result) client_store_stats(client, result);
			json_value_free(json);
			continue;
		}

		if(!method)
		{
			json_value_free(json);
			clientlog(client, "bad json, no method");
			break;
		}

		json_value *json_params = json_get_array(json, "params");
		if(!json_params)
		{
			json_value_free(json);
			clientlog(client, "bad json, no params");
			break;
		}

#ifdef CLIENT_DEBUGLOG_
		debuglog("client %s %d %s\n", method, client->id_int, client->id_str? client->id_str: "null");
#endif

		bool b = false;
		if(!strcmp(method, "mining.subscribe"))
			b = client_subscribe(client, json_params);

		else if(!strcmp(method, "mining.authorize"))
			b = client_authorize(client, json_params);

		else if(!strcmp(method, "mining.ping"))
			b = client_send_result(client, "\"pong\"");

		else if(!strcmp(method, "mining.submit"))
			b = client_submit(client, json_params);

		else if(!strcmp(method, "mining.suggest_difficulty"))
			b = client_suggest_difficulty(client, json_params);

		else if(!strcmp(method, "mining.suggest_target"))
			b = client_suggest_target(client, json_params);

		else if(!strcmp(method, "mining.get_transactions"))
			b = client_send_result(client, "[]");

		else if(!strcmp(method, "mining.multi_version"))
			b = client_send_result(client, "false"); // ASICBOOST

		else if(!strcmp(method, "mining.extranonce.subscribe"))
		{
			client->extranonce_subscribe = true;
			b = client_send_result(client, "true");
		}

		else if(!strcmp(method, "mining.update_block"))
			client_update_block(client, json_params);

		else if(!strcmp(method, "getwork"))
		{
			clientlog(client, "using getwork"); // client using http:// url
			break;
		}

		else
		{
			b = client_send_error(client, 20, "Not supported");
			client->submit_bad++;

			stratumlog("unknown method %s %s\n", method, client->sock->ip);
		}

		json_value_free(json);
		if(!b) break;
	}

//	source_close(client->source);

#ifdef CLIENT_DEBUGLOG_
	debuglog("client terminate\n");
#endif
	if(!client || client->deleted) {
		pthread_exit(NULL);
	}

	else if(client->sock->total_read == 0)
		clientlog(client, "no data");

	if(client->sock->sock >= 0)
		shutdown(client->sock->sock, SHUT_RDWR);

	if(g_list_client.Find(client))
	{
		if(client->workerid && !client->reconnecting)
		{
			CommonLock(&g_db_mutex);
			db_clear_worker(g_db, client);
			CommonUnlock(&g_db_mutex);
		}
	}

	object_delete(client);

	pthread_exit(NULL);
}

