/*******************************************************************************
 * Copyright 2013-2021 Aerospike, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

#include <Python.h>
#include <stdbool.h>

#include <aerospike/aerospike_info.h>
#include <aerospike/as_key.h>
#include <aerospike/as_error.h>
#include <aerospike/as_node.h>
#include <aerospike/as_cluster.h>

#include "client.h"
#include "conversions.h"
#include "exceptions.h"
#include "policy.h"

static char *get_unbracketed_ip_and_length(char *ip_start, char *split_point,
                                           int *length);

/**
 ******************************************************************************************************
	* Returns data about the nodes to AerospikeClient_GetNodes.
	*
	* @param self                  AerospikeClient object
	*
	* Returns a list containing the IP and port tuple of each node.
	********************************************************************************************************/
static PyObject *AerospikeClient_GetNodes_Invoke(AerospikeClient *self)
{

    PyObject *py_hostname = NULL;
    PyObject *py_port = NULL;
    PyObject *return_value = PyList_New(0);

    as_nodes *nodes = NULL;
    char *hostname = NULL;
    char *split_point = NULL;

    as_error err;
    as_error_init(&err);

    if (!self || !self->as) {
        as_error_update(&err, AEROSPIKE_ERR_PARAM, "Invalid aerospike object");
        goto CLEANUP;
    }

    if (!self->is_conn_16) {
        as_error_update(&err, AEROSPIKE_ERR_CLUSTER,
                        "No connection to aerospike cluster");
        goto CLEANUP;
    }

    aerospike *as = self->as;
    as_cluster *cluster = as->cluster;
    // If the cluster goes down between the last call and this call, this could theoretically occur.
    if (!cluster) {
        as_error_update(&err, AEROSPIKE_ERR_CLUSTER,
                        "invalid aerospike cluster");
        goto CLEANUP;
    }

    nodes = as_nodes_reserve(cluster);

    if (!nodes) {
        as_error_update(&err, AEROSPIKE_ERR_CLUSTER, "Cluster is empty");
        goto CLEANUP;
    }

    for (uint32_t i = 0; i < nodes->size; i++) {
        as_node *node = nodes->array[i];
        hostname = (char *)as_node_get_address_string(node);
        split_point = strrchr(hostname, ':');
        if (!split_point) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Malformed host name string");
            goto CLEANUP;
        }

        char *real_hostname_start = NULL;
        int real_length;
        /* Since hostname might be '[::1]' and we want to return only '::1' , we need to set
			* a pointer
			*/
        real_hostname_start =
            get_unbracketed_ip_and_length(hostname, split_point, &real_length);
        Py_ssize_t py_host_length = (Py_ssize_t)real_length;
        py_hostname =
            PyUnicode_FromStringAndSize(real_hostname_start, py_host_length);

        if (!py_hostname) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to create python hostname");
            goto CLEANUP;
        }

        // convert "3000" -> 3000, using base 10 | use long since it works in 2 & 3
        py_port = PyLong_FromString(split_point + 1, NULL, 10);
        if (!py_port || PyErr_Occurred()) {
            // py_port exists
            Py_XDECREF(py_hostname);
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Non numeric port found");
            goto CLEANUP;
        }
        PyObject *py_host_pair = Py_BuildValue("OO", py_hostname, py_port);

        Py_XDECREF(py_port);
        Py_XDECREF(py_hostname);

        if (!py_host_pair) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to build node info tuple");
            goto CLEANUP;
        }

        PyList_Append(return_value, py_host_pair);
        Py_DECREF(py_host_pair);
    }
CLEANUP:
    if (nodes) {
        as_nodes_release(nodes);
    }
    if (err.code != AEROSPIKE_OK) {
        // Clear the return value if it exists
        Py_XDECREF(return_value);
        raise_exception(&err);

        return NULL;
    }

    return return_value;
}

/******************************************************************************************************
 * Returns data about the nodes in a cluster of the database.
 *
 * @param self                  AerospikeClient object
 * @param args                  The args is a tuple object containing an argument
 *                              list passed from Python to a C function
 * @param kwds                  Dictionary of keywords
 *
 * Returns a list containing the IP and port tuple of each node.
 ********************************************************************************************************/
PyObject *AerospikeClient_GetNodes(AerospikeClient *self, PyObject *args,
                                   PyObject *kwds)
{
    return AerospikeClient_GetNodes_Invoke(self);
}

/**
 ******************************************************************************************************
	* Returns data about the nodes to AerospikeClient_GetNodeNames.
	*
	* @param self                  AerospikeClient object
	*
	* Returns a list containing the IP, port, name dict of each node.
	********************************************************************************************************/
static PyObject *AerospikeClient_GetNodeNames_Invoke(AerospikeClient *self)
{

    PyObject *py_node_name = NULL;
    PyObject *py_hostname = NULL;
    PyObject *py_port = NULL;
    PyObject *py_return_dict = NULL;
    PyObject *return_value = PyList_New(0);

    as_nodes *nodes = NULL;
    char *hostname = NULL;
    char *split_point = NULL;

    as_error err;
    as_error_init(&err);

    if (!self || !self->as) {
        as_error_update(&err, AEROSPIKE_ERR_PARAM, "Invalid aerospike object.");
        goto CLEANUP;
    }

    if (!self->is_conn_16) {
        as_error_update(&err, AEROSPIKE_ERR_CLUSTER,
                        "No connection to aerospike cluster.");
        goto CLEANUP;
    }

    aerospike *as = self->as;
    as_cluster *cluster = as->cluster;
    // If the cluster goes down between the last call and this call, this could theoretically occur.
    if (!cluster) {
        as_error_update(&err, AEROSPIKE_ERR_CLUSTER,
                        "invalid aerospike cluster.");
        goto CLEANUP;
    }

    nodes = as_nodes_reserve(cluster);

    if (!nodes) {
        as_error_update(&err, AEROSPIKE_ERR_CLUSTER, "Cluster is empty.");
        goto CLEANUP;
    }

    for (uint32_t i = 0; i < nodes->size; i++) {
        as_node *node = nodes->array[i];
        hostname = (char *)as_node_get_address_string(node);
        split_point = strrchr(hostname, ':');
        if (!split_point) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Malformed host name string.");
            goto CLEANUP;
        }

        char *real_hostname_start = NULL;
        int real_length;
        /* Since hostname might be '[::1]' and we want to return only '::1' , we need to set
			* a pointer
			*/
        real_hostname_start =
            get_unbracketed_ip_and_length(hostname, split_point, &real_length);
        Py_ssize_t py_host_length = (Py_ssize_t)real_length;
        py_hostname =
            PyUnicode_FromStringAndSize(real_hostname_start, py_host_length);

        if (!py_hostname) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to create python hostname.");
            goto CLEANUP;
        }

        // convert "3000" -> 3000, using base 10 | use long since it works in 2 & 3
        py_port = PyLong_FromString(split_point + 1, NULL, 10);
        if (!py_port || PyErr_Occurred()) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Non numeric port found.");
            goto CLEANUP;
        }

        py_node_name = PyUnicode_FromString(node->name);
        if (py_node_name == NULL) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to get node name.");
            goto CLEANUP;
        }

        const char *hostname_key = "address";
        const char *port_key = "port";
        const char *node_name_key = "node_name";
        py_return_dict = PyDict_New();
        if (!py_return_dict) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to build node info dictionary.");
            goto CLEANUP;
        }

        if (PyDict_SetItemString(py_return_dict, hostname_key, py_hostname) ==
                -1 ||
            PyDict_SetItemString(py_return_dict, port_key, py_port) == -1 ||
            PyDict_SetItemString(py_return_dict, node_name_key, py_node_name) ==
                -1) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to add dictionary item.");
            goto CLEANUP;
        }

        if (PyList_Append(return_value, py_return_dict) == -1) {
            as_error_update(&err, AEROSPIKE_ERR_CLIENT,
                            "Failed to append py_return_dict to return_value.");
            goto CLEANUP;
        }
    }
CLEANUP:
    if (nodes) {
        as_nodes_release(nodes);
    }

    Py_XDECREF(py_port);
    Py_XDECREF(py_hostname);
    Py_XDECREF(py_node_name);
    Py_XDECREF(py_return_dict);

    if (err.code != AEROSPIKE_OK) {
        // Clear the return value if it exists
        Py_XDECREF(return_value);
        raise_exception(&err);

        return NULL;
    }

    return return_value;
}

/******************************************************************************************************
 * Returns data about the nodes in a cluster of the database.
 *
 * @param self                  AerospikeClient object
 * @param args                  The args is a tuple object containing an argument
 *                              list passed from Python to a C function
 * @param kwds                  Dictionary of keywords
 *
 * Returns a list containing the IP, port, name dict of each node.
 ********************************************************************************************************/
PyObject *AerospikeClient_GetNodeNames(AerospikeClient *self, PyObject *args,
                                       PyObject *kwds)
{
    return AerospikeClient_GetNodeNames_Invoke(self);
}

/*
 * This will return the correct starting point of an ipv6 address wrapped in [] and the length of the actual ip
 * split_point is the address of the rightmost : in the ip_start string
 * so something like ip_start = "[::1]:3000"
 * where ip_start is 0xf0 and split_point is 0xf5
 * len will be set to 3 and the return value will be (char*)0xf1
 *
 * If this is ipv4, length is the length of the address, and returned value = ip_start
 */
static char *get_unbracketed_ip_and_length(char *ip_start, char *split_point,
                                           int *length)
{
    *length = split_point - ip_start;
    if (*length < 2) {
        return ip_start;
    }
    if (ip_start[0] == '[' && ip_start[*length - 1] == ']') {
        *length = *length - 2;
        return (char *)(ip_start + 1);
    }
    return ip_start;
}
