/**
 *	Send an info request to the entire cluster.
 *
 *	~~~~~~~~~~{.c}
 *	if ( aerospike_info_foreach(&as, &err, NULL, "info", callback, NULL) != AEROSPIKE_OK ) {
 *		// handle error
 *	}
 *	~~~~~~~~~~
 *
 *	The callback takes a response string. The caller should not free this string.
 *
 *	~~~~~~~~~~{.c}
 *	bool callback(const as_error * err, const char * node, char * res, void * udata) {
 *		// handle response
 *	}
 *	~~~~~~~~~~
 *
 *
 *	@param as			The aerospike instance to use for this operation.
 *	@param err			The as_error to be populated if an error occurs.
 *	@param policy		The policy to use for this operation. If NULL, then the default policy will be used.
 *	@param req			The info request to send.
 *	@param callback		The function to call when a response is received.
 *	@param udata		User-data to send to the callback.
 *
 *	@return AEROSPIKE_OK on success. Otherwise an error.
 *
 *	@ingroup info_operations
 */
as_status aerospike_info_foreach(
	aerospike * as, as_error * err, const as_policy_info * policy, 
	const char * req, 
	aerospike_info_foreach_callback callback, void * udata)
{
	// we want to reset the error so, we have a clean state
	as_error_reset(err);
	
	// resolve policies
	as_policy_info p;
	as_policy_info_resolve(&p, &as->config.policies, policy);
	
	if ( !as ) {
		return AEROSPIKE_ERR;
	}

	citrusleaf_info_cluster_foreach_data data = {
		.callback = callback,
		.udata = udata
	};
	
	char* error = 0;

	int rc = citrusleaf_info_cluster_foreach(
		as->cluster, req, p.send_as_is, p.check_bounds, p.timeout, (void *) &data, &error,
		citrusleaf_info_cluster_foreach_callback);

	if (rc) {
		as_strncpy(err->message, error, sizeof(err->message));
		free(error);
		return as_error_fromrc(err, rc);
	}
	return AEROSPIKE_OK;
}
/**
 *	Send an info request to a specific host. The response must be freed by the caller on success.
 *
 *	~~~~~~~~~~{.c}
 *	char * res = NULL;
 *	if ( aerospike_info_host(&as, &err, NULL, "127.0.0.1", 3000, "info", &res) != AEROSPIKE_OK ) {
 *		// handle error
 *	}
 *	else {
 *		// handle response
 *		free(res);
 *		res = NULL;
 *	}
 *	~~~~~~~~~~
 *
 *	@param as			The aerospike instance to use for this operation.
 *	@param err			The as_error to be populated if an error occurs.
 *	@param policy		The policy to use for this operation. If NULL, then the default policy will be used.
 *	@param addr			The IP address or hostname to send the request to.
 *	@param port			The port to send the request to.
 *	@param req			The info request to send.
 *	@param res			The response from the node. The response will be a NULL terminated string, allocated by the function, and must be freed by the caller.
 *
 *	@return AEROSPIKE_OK on success. Otherwise an error.
 *
 *	@ingroup info_operations
 */
as_status aerospike_info_host(
	aerospike * as, as_error * err, const as_policy_info * policy, 
	const char * addr, uint16_t port, const char * req, 
	char ** res) 
{
	// we want to reset the error so, we have a clean state
	as_error_reset(err);
	
	// resolve policies
	as_policy_info p;
	as_policy_info_resolve(&p, &as->config.policies, policy);

	if (! as) {
		return AEROSPIKE_ERR;
	}

	cl_rv rc = citrusleaf_info_auth(as->cluster, (char *) addr, port, (char *) req, res, p.timeout);

	if (rc) {
		as_strncpy(err->message, *res, sizeof(err->message));
		free(*res);
		return as_error_fromrc(err, rc);
	}
		
	return AEROSPIKE_OK;
}
static bool scan_udf_info_callback(const as_error * err, const as_node * node, const char * req, char * res, void * udata) {

	if (!err) {
		goto done;
	}

	if ( err->code != AEROSPIKE_OK ) {
		debug("UDF_CALLBACK Error: (%d) %s - node=%s response=%s\n", err->code, err->message, node ? node->name : "NULL", res);
	}
	else {
		if ( res == NULL || strlen(res) == 0 ) {
			goto done;
		}
		char * start_resp = strchr(res, '\t');

		if ( start_resp == NULL || strlen(start_resp) == 0 ) {
			goto done;
		}
		char print_resp[128];
		as_strncpy(print_resp, start_resp, sizeof(print_resp));
	 	debug("%s", print_resp);
	}
done:
	return true;
}
bool
as_config_set_user(as_config* config, const char* user, const char* password)
{
	if (user && *user) {
		if (as_strncpy(config->user, user, sizeof(config->user))) {
			return false;
		}
		
		return as_password_get_constant_hash(password, config->password);
	}
	else {
		return false;
	}
}
bool
as_password_prompt_hash(const char* password, char* hash)
{
	char pass[AS_PASSWORD_HASH_SIZE];
	
	if (password && *password) {
		as_strncpy(pass, password, sizeof(pass));
	}
	else {
		as_password_prompt(pass, sizeof(pass));
	}
	
	return as_password_get_constant_hash(pass, hash);
}
/**
 * Create a new secondary index.
 *
 * @param as        - the aerospike cluster to connect to.
 * @param err       - the error is populated if the return value is not AEROSPIKE_OK.
 * @param policy    - the policy to use for this operation. If NULL, then the default policy will be used.
 * @param ns        - the namespace to be indexed
 * @param set       - the set to be indexed
 * @param bin       - the bin to be indexed
 * @param type      - the type of the bin to be indexed
 * @param name      - the name of the index
 *
 * @return AEROSPIKE_OK if successful or index already exists. Otherwise an error.
 */
static as_status aerospike_index_create(
	aerospike * as, as_error * err, const as_policy_info * policy, 
	const char * ns, const char * set, const char * bin, const char * type, const char * name)
{
	as_error_reset(err);

	char * response = NULL;
	int rc = citrusleaf_secondary_index_create(as->cluster, ns, set, name, bin, type, &response);
	
	switch ( rc ) {
		case CITRUSLEAF_OK:
		case CITRUSLEAF_FAIL_INDEX_FOUND:
			break;

		default:
			as_strncpy(err->message, response, sizeof(err->message));
			as_error_fromrc(err, rc);
			break;
	}

	free(response);
	return err->code;
}
/**
 *	Create secondary index.
 *
 *	This asynchronous server call will return before the command is complete.
 *	The user can optionally wait for command completion by using a task instance.
 *
 *	~~~~~~~~~~{.c}
 *	as_index_task task;
 *	if ( aerospike_index_create(&as, &err, &task, NULL, "test", "demo", "bin1", "idx_test_demo_bin1") == AEROSPIKE_OK ) {
 *		aerospike_index_create_wait(&err, &task, 0);
 *	}
 *	~~~~~~~~~~
 *
 *	@param as			The aerospike instance to use for this operation.
 *	@param err			The as_error to be populated if an error occurs.
 *	@param task			The optional task data used to poll for completion.
 *	@param policy		The policy to use for this operation. If NULL, then the default policy will be used.
 *	@param ns			The namespace to be indexed.
 *	@param set			The set to be indexed.
 *	@param bin			The bin to be indexed.
 *	@param name			The name of the index.
 *
 *	@return AEROSPIKE_OK if successful or index already exists. Otherwise an error.
 *
 *	@ingroup index_operations
 */
as_status aerospike_index_create_complex(
	aerospike * as, as_error * err, as_index_task * task, const as_policy_info * policy,
	const as_namespace ns, const as_set set, const as_index_position position, const char * name,
	as_index_type itype, as_index_datatype dtype)
{
	as_error_reset(err);
	
	const char* dtype_string;
    switch (dtype) {
    case AS_INDEX_NUMERIC:
        dtype_string = "NUMERIC";
        break;
    case AS_INDEX_GEO2DSPHERE:
        dtype_string = "GEO2DSPHERE";
        break;
    default:
    case AS_INDEX_STRING:
        dtype_string = "STRING";
        break;
    }

	const char* itype_string;
	switch (itype) {
		default:
		case AS_INDEX_TYPE_DEFAULT: {
			itype_string = "DEFAULT";
			break;
		}
		case AS_INDEX_TYPE_LIST: {
			itype_string = "LIST";
			break;
		}
		case AS_INDEX_TYPE_MAPKEYS: {
			itype_string = "MAPKEYS";
			break;
		}
		case AS_INDEX_TYPE_MAPVALUES: {
			itype_string = "MAPVALUES";
			break;
		}
	}

    char ddl[1024];
    
	if (itype == AS_INDEX_TYPE_DEFAULT) {
		// Use old format, so command can work with older servers.
		sprintf(ddl,
				"sindex-create:ns=%s%s%s;indexname=%s;"
				"numbins=1;indexdata=%s,%s;priority=normal\n",
				ns, set ? ";set=" : "", set ? set : "",
				name, position, dtype_string
				);
	}
	else {
		// Use new format.
		sprintf(ddl,
				"sindex-create:ns=%s%s%s;indexname=%s;"
				"numbins=1;indextype=%s;indexdata=%s,%s;priority=normal\n",
				ns, set ? ";set=" : "", set ? set : "",
				name, itype_string, position, dtype_string
				);
	}

	char* response = NULL;
	as_status status = aerospike_info_any(as, err, policy, ddl, &response);
	
	switch (status) {
		case AEROSPIKE_OK:
			// Return task that could optionally be polled for completion.
			if (task) {
				task->as = as;
				as_strncpy(task->ns, ns, sizeof(task->ns));
				as_strncpy(task->name, name, sizeof(task->name));
				task->done = false;
			}
			cf_free(response);
			break;
			
		case AEROSPIKE_ERR_INDEX_FOUND:
			// Index has already been created.  Do not need to poll for completion.
			if (task) {
				task->done = true;
			}
			status = AEROSPIKE_OK;
			as_error_reset(err);
			break;
			
		default:
			break;
	}
	return status;
}
static as_status
as_cluster_seed_nodes(as_cluster* cluster, as_error* err, bool enable_warnings)
{
	// Add all nodes at once to avoid copying entire array multiple times.
	as_vector nodes_to_add;
	as_vector_inita(&nodes_to_add, sizeof(as_node*), 64);
	
	as_vector addresses;
	as_vector_inita(&addresses, sizeof(struct sockaddr_in), 5);
	
	as_node_info node_info;
	as_error error_local;
	as_error_init(&error_local); // AEROSPIKE_ERR_TIMEOUT doesn't come with a message; make sure it's initialized.
	as_status status = AEROSPIKE_OK;
	
	as_seeds* seeds = as_seeds_reserve(cluster);

	for (uint32_t i = 0; i < seeds->size; i++) {
		as_seed* seed = &seeds->array[i];
		as_vector_clear(&addresses);
		
		status = as_lookup(&error_local, seed->name, seed->port, &addresses);
		
		if (status != AEROSPIKE_OK) {
			if (enable_warnings) {
				as_log_warn("Failed to lookup %s:%d. %s %s", seed->name, seed->port, as_error_string(status), error_local.message);
			}
			continue;
		}

		for (uint32_t i = 0; i < addresses.size; i++) {
			struct sockaddr_in* addr = as_vector_get(&addresses, i);
			status = as_lookup_node(cluster, &error_local, addr, &node_info);
			
			if (status == AEROSPIKE_OK) {
				as_host host;
				if (as_strncpy(host.name, seed->name, sizeof(host.name))) {
					as_log_warn("Hostname has been truncated: %s", host.name);
				}
				host.port = seed->port;

				as_node* node = as_cluster_find_node_in_vector(&nodes_to_add, node_info.name);
				
				if (node) {
					as_close(node_info.fd);
					as_node_add_address(node, &host, addr);
				}
				else {
					node = as_node_create(cluster, &host, addr, &node_info);
					as_address* a = as_node_get_address_full(node);
					as_log_info("Add node %s %s:%d", node->name, a->name, (int)cf_swap_from_be16(a->addr.sin_port));
					as_vector_append(&nodes_to_add, &node);
				}
			}
			else {
				if (enable_warnings) {
					as_log_warn("Failed to connect to seed %s:%d. %s %s", seed->name, seed->port, as_error_string(status), error_local.message);
				}
			}
		}
	}
	
	as_seeds_release(seeds);

	if (nodes_to_add.size > 0) {
		as_cluster_add_nodes(cluster, &nodes_to_add);
		status = AEROSPIKE_OK;
	}
	else {
		status = as_error_set_message(err, AEROSPIKE_ERR_CLIENT, "Failed to seed cluster");
	}
	
	as_vector_destroy(&nodes_to_add);
	as_vector_destroy(&addresses);
	return status;
}
static as_status
as_lookup_node(as_cluster* cluster, as_error* err, struct sockaddr_in* addr, as_node_info* node_info)
{
	uint64_t deadline = as_socket_deadline(cluster->conn_timeout_ms);
	int fd;
	as_status status = as_info_create_socket(cluster, err, addr, deadline, &fd);

	if (status) {
		return status;
	}
	
	char* response = 0;
	status = as_info_command(err, fd, "node\nfeatures\n", true, deadline, 0, &response);

	if (status) {
		as_close(fd);
		return status;
	}
	
	as_vector values;
	as_vector_inita(&values, sizeof(as_name_value), 2);
	
	as_info_parse_multi_response(response, &values);
	
	if (values.size != 2) {
		goto Error;
	}
	
	as_name_value* nv = as_vector_get(&values, 0);
	char* node_name = nv->value;
	
	if (node_name == 0 || *node_name == 0) {
		goto Error;
	}
	as_strncpy(node_info->name, node_name, AS_NODE_NAME_SIZE);
	node_info->fd = fd;

	nv = as_vector_get(&values, 1);
	char* features = nv->value;
	
	if (features == 0) {
		goto Error;
	}
			
	char* begin = features;
	char* end = begin;
	uint8_t has_batch_index = 0;
	uint8_t has_replicas_all = 0;
	uint8_t has_double = 0;
	uint8_t has_geo = 0;
	
	while (*begin && ! (has_batch_index &&
						has_replicas_all &&
						has_double &&
						has_geo)) {
		while (*end) {
			if (*end == ';') {
				*end++ = 0;
				break;
			}
			end++;
		}
		
		if (strcmp(begin, "batch-index") == 0) {
			has_batch_index = 1;
		}
		
		if (strcmp(begin, "replicas-all") == 0) {
			has_replicas_all = 1;
		}
		
		if (strcmp(begin, "float") == 0) {
			has_double = 1;
		}

		if (strcmp(begin, "geo") == 0) {
			has_geo = 1;
		}

		begin = end;
	}
	node_info->has_batch_index = has_batch_index;
	node_info->has_replicas_all = has_replicas_all;
	node_info->has_double = has_double;
	node_info->has_geo = has_geo;
	cf_free(response);
	return AEROSPIKE_OK;
	
Error: {
		char addr_name[INET_ADDRSTRLEN];
		as_socket_address_name(addr, addr_name);
		as_error_update(err, status, "Invalid node info response from %s: %s", addr_name, response);
		cf_free(response);
		as_close(fd);
		return AEROSPIKE_ERR_CLIENT;
	}
}
/**
 * @return AEROSPIKE_OK if successful. Otherwise an error occurred.
 */
as_status aerospike_udf_list(
	aerospike * as, as_error * err, const as_policy_info * policy, 
	as_udf_files * files)
{
	as_error_reset(err);
	
	if (! policy) {
		policy = &as->config.policies.info;
	}

	char* response = 0;
	as_status status = aerospike_info_any(as, err, policy, "udf-list", &response);
	
	if (status) {
		return status;
	}
	
	// response := udf-list\tfilename=<name>,hash=<hash>,type=<type>;[filename=<name>...]
	char* p = strchr(response, '\t');
	
	if (!p) {
		as_error_update(err, AEROSPIKE_ERR_PARAM, "Invalid udf-list response: %s", response);
		free(response);
		return AEROSPIKE_ERR_PARAM;
	}
	p++;
	
	uint32_t capacity = (files->capacity <= 0) ? 500 : files->capacity;
	
	as_vector ptrs;
	as_vector_inita(&ptrs, sizeof(as_udf_file_ptr), capacity);
	
	as_udf_file_ptr ptr = {0,0,0};
	char* token = p;
	
	while (*p) {
		switch (*p) {
			case '=':
				*p++ = 0;
				as_udf_parse_file(token, p, &ptr);
				break;
	
			case ',':
				*p++ = 0;
				token = p;
				break;
				
			case ';':
				*p++ = 0;
				token = p;
				as_vector_append(&ptrs, &ptr);
				ptr.name = 0;
				ptr.hash = 0;
				ptr.type = 0;
				break;
				
			default:
				p++;
				break;
		}
	}
	
	if (files->capacity == 0 && files->entries == NULL) {
		as_udf_files_init(files, ptrs.size);
	}

	uint32_t limit = ptrs.size < files->capacity ? ptrs.size : files->capacity;
	files->size = limit;

	for (uint32_t i = 0; i < limit; i++) {
		as_udf_file_ptr* ptr = as_vector_get(&ptrs, i);
		as_udf_file* file = &files->entries[i];
		
		if (ptr->name) {
			as_strncpy(file->name, ptr->name, AS_UDF_FILE_NAME_SIZE);
		}
		else {
			file->name[0] = 0;
		}
		
		if (ptr->hash) {
			// The hash is not null terminated, so normal strncpy is appropriate here.
			// strncpy will also pad zeroes if hash size is incorrect.
			strncpy((char*)file->hash, ptr->hash, AS_UDF_FILE_HASH_SIZE);
		}
		else {
			file->hash[0] = 0;
		}
		
		file->type = AS_UDF_TYPE_LUA;
		file->content._free = false;
		file->content.size = 0;
		file->content.capacity = 0;
		file->content.bytes = NULL;
	}

	as_vector_destroy(&ptrs);
	free(response);
	return AEROSPIKE_OK;
}
/**
 * @return AEROSPIKE_OK if successful. Otherwise an error occurred.
 */
as_status aerospike_udf_get(
	aerospike * as, as_error * err, const as_policy_info * policy, 
	const char * filename, as_udf_type type, as_udf_file * file)
{
	as_error_reset(err);
	
	if (! policy) {
		policy = &as->config.policies.info;
	}
		
	char command[512];
	snprintf(command, sizeof(command), "udf-get:filename=%s;", filename);
	
	char* response = 0;
	as_status status = aerospike_info_any(as, err, policy, command, &response);
	
	if (status) {
		return status;
	}
	
	// response := <command>\tgen=<string>;type=<string>;content=<string>;
	char* p = strchr(response, '\t');
	
	if (!p) {
		as_error_update(err, AEROSPIKE_ERR_PARAM, "Invalid udf-get response: %s", response);
		free(response);
		return AEROSPIKE_ERR_PARAM;
	}
	p++;
	
	p = strstr(p, "content=");
	
	if (!p) {
		as_error_update(err, AEROSPIKE_ERR_PARAM, "Invalid udf-get response: %s", response);
		free(response);
		return AEROSPIKE_ERR_PARAM;
	}
	p += 8;
	
	as_strncpy(file->name, filename, AS_UDF_FILE_NAME_SIZE);
	file->type = AS_UDF_TYPE_LUA;

	char* content = p;
	while (*p) {
		if (*p == ';') {
			*p = 0;
			break;
		}
		p++;
	}
	
	uint32_t len = (uint32_t)(p - content);
	uint32_t size;
	cf_b64_validate_and_decode_in_place((uint8_t*)content, len, &size);

	// Update file hash
	unsigned char hash[SHA_DIGEST_LENGTH];
#ifdef __APPLE__
	// Openssl is deprecated on mac, but the library is still included.
	// Save old settings and disable deprecated warnings.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
	SHA1((uint8_t*)content, size, hash);
#ifdef __APPLE__
	// Restore old settings.
#pragma GCC diagnostic pop
#endif
	cf_convert_sha1_to_hex(hash, file->hash);

	file->content._free = true;
	file->content.size = size;
	file->content.capacity = size;
	file->content.bytes = malloc(size);
	memcpy(file->content.bytes, content, size);
	
	free(response);
	return AEROSPIKE_OK;
}