#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import math
import subprocess
from gspylib.common.ErrorCode import ErrorCode
from gspylib.common.common.default_value import DefaultValue
from gspylib.common.cluster_topology.parse_xml import ParseXml

parse_xml = ParseXml()
# min comm_max_stream
MIN_COMM_MAX_STREAM = 1024
# max comm_max_stream
MAX_COMM_MAX_STREAM = 60000
# min dn max_connections
MIN_COMM_MAX_CONNECTIONS = 5000


class DynamicGucConfig:

    def __init__(self):
        pass

    @staticmethod
    def dynamic_guc(instance_type, config_item_type, cluster_info, min_value, inst_Info):
        """
        function: set hba config
        input : NA
        output: NA
        """
        # getting the path of guc_list.conf.
        dir_name = os.path.dirname(os.path.realpath(__file__))
        if config_item_type == DefaultValue.CLUSTER_TYPE_SINGLE_INST:
            guc_file = os.path.join(dir_name, "../../etc/conf/guc_single_list.xml")
            guc_file = os.path.normpath(guc_file)
        else:
            guc_file = os.path.join(dir_name, "../../etc/conf/guc_list.xml")
            guc_file = os.path.normpath(guc_file)

        # reading xml.
        guc_dict = {}
        root_node = parse_xml.initParserXMLFile(guc_file)
        instance_ele = root_node.find(instance_type)
        instance_list = instance_ele.findall("PARAM")
        for guc_element in instance_list:
            DynamicGucConfig.check_guc(guc_element.attrib['VALUE'])
            guc_dict[guc_element.attrib['KEY']] = guc_element.attrib['VALUE']

        guc_para_dict = DynamicGucConfig.init_guc(guc_dict, config_item_type, cluster_info, min_value)
        if instance_type == "dn":
            numa_bind_node = DynamicGucConfig.get_numa_bind_node(cluster_info, inst_Info)
            guc_para_dict.update(numa_bind_node)

        return guc_para_dict

    @staticmethod
    def get_numa_bind_node(cluster_info, inst_Info):
        cpu_list = os.path.join(cluster_info.appPath, "etc", "cpulist")
        if not os.path.exists(cpu_list):
            return {"numa_bind_node": "'none'"}

        with open(cpu_list, "r") as fp:
            cpu_list_info = fp.read().splitlines()

        node_info = cluster_info.getDbNodeByName(inst_Info.hostname)
        if node_info is None:
            return {"numa_bind_node": "'none'"}

        numa_bind_node_value = DefaultValue.get_numa_bind_node_value(node_info, inst_Info, cpu_list_info)

        return {"numa_bind_node": numa_bind_node_value}

    @staticmethod
    def check_guc(guc_value):
        """
        function: check path vaild
        input : envValue
        output: NA
        """
        guc_check_list = ["|", ";", "&", "$", "<", ">", "`", "{", "}", "[", "]", "~", "?", " ", "!"]
        if guc_value.strip() == "":
            return
        for rac in guc_check_list:
            flag = guc_value.find(rac)
            if flag >= 0:
                raise Exception(ErrorCode.GAUSS_502["GAUSS_50219"] % guc_value +
                                " There are illegal characters %s in the content." % rac)

    @staticmethod
    def init_guc(guc_dict, config_item_type, cluster_info, min_value):
        """
        """
        dynamic_guc_dict = dict()
        guc_dict = DynamicGucConfig.get_common_guc_value(guc_dict, cluster_info, min_value)

        if not config_item_type == DefaultValue.CLUSTER_TYPE_SINGLE_INST:
            dynamic_guc_dict = DynamicGucConfig.get_dynamic_guc_value(min_value, cluster_info, guc_dict)
        dynamic_guc_dict.update(guc_dict)
        related_guc_dict = DynamicGucConfig.get_related_guc_value(dynamic_guc_dict, guc="max_process_memory")
        if "work_mem" in related_guc_dict.keys():
            related_guc_dict["maintenance_work_mem"] = related_guc_dict.get("work_mem")

        related_guc_dict['time_track_strategy'] = "tsc" if DefaultValue.support_tsc() else "vector"

        return related_guc_dict

    @staticmethod
    def get_common_guc_value(guc_dict, cluster_info, min_value):
        """
        get base guc: comm_max_datanode, max_process_memory, max_process_memory_balanced
        value:
        comm_max_datanode: The cluster total DN num
        max_process_memory: PHYSIC_MEMORY*0.8/(1+dn_num_in_one_node)
        max_process_memory_balanced: (PHYSIC_MEMORY - RESERVE_MEMORY - CN_max_process_memory) / dn_num_in_one_node
        """
        for guc in guc_dict:
            if guc == "comm_max_datanode":
                total_data_node_num = cluster_info.get_total_data_node_dum()
                if os.getenv("CONTAINER_WORKPLACE"):
                    total_data_node_num = total_data_node_num * 3
                    guc_dict[guc] = guc_dict.get(guc).replace("DATANUM_ALLNODES", str(total_data_node_num))
                else:
                    guc_dict[guc] = guc_dict.get(guc).replace("DATANUM_ALLNODES", str(total_data_node_num))
                continue
            elif guc == "max_process_memory":
                DynamicGucConfig.get_guc_value_4_max_process_memory(guc_dict, cluster_info, min_value)
                continue
            elif guc == "max_process_memory_balanced":
                DynamicGucConfig.get_guc_value_4_max_process_memory_balanced(guc_dict, cluster_info, min_value)
                continue
        return guc_dict

    @staticmethod
    def get_memory_value_from_proc_meminfo(memory_type):
        (returncode, result) = subprocess.getstatusoutput("cat /proc/meminfo | grep -w %s" % memory_type)
        if returncode > 0:
            # failed to get shared memory from os, need to print warning message
            memory_value = 0
        else:
            try:
                result_list = result.strip().split(' ')
                value = int(result_list[len(result_list) - 2])
                factor = result_list[len(result_list) - 1]
                if factor == 'kB':
                    memory_value = int(round(value / 1024 / 1024))
                elif (factor == ''):
                    memory_value = int(round(value / 1024 / 1024 / 1024))
            except Exception as e:
                raise Exception(ErrorCode.GAUSS_505["GAUSS_50502"] % memory_type + " Error: \n%s" % str(e))
        return memory_value

    @staticmethod
    def get_reserve_memory(cluster_info, min_physical_memory):
        """
        os reserve memory: total memory - shared memory - cache memory
        manage service reserve memory: dms_gent, gtm, cm_agent, etc
        reserve memory: os reserve memory + manage service reserve memory
        """
        # htap cluster do not need reserve memory
        if cluster_info.isHtapCluster():
            return 0

        # physical memory less than 8GB, set reserve memory to 2GB at least.
        if min_physical_memory < 8:
            return 2

        # get os shared memory
        shared_memory = DynamicGucConfig.get_memory_value_from_proc_meminfo("Shmem")

        # set os_reserve_memory to 2GB at least
        os_reserve_memory = min(shared_memory, 2)

        # reserve some memory for dws maintain service, such as: dms_agent, cm_agent, gtm, etc
        if min_physical_memory <= 32:
            manage_reserve_memory = 4
        elif min_physical_memory <= 64:
            manage_reserve_memory = 8
        elif min_physical_memory <= 128:
            manage_reserve_memory = 16
        else:
            manage_reserve_memory = 32

        # dws-bigdata total memory: min_physical_memory divide 8
        dws_bigdata_memory = round(min_physical_memory / 8)

        return os_reserve_memory + manage_reserve_memory + dws_bigdata_memory

    @staticmethod
    def get_cn_max_process_memory(min_physical_memory, max_master_datanum_in_onenode):
        return min_physical_memory * 0.4 / (1 + max_master_datanum_in_onenode)

    @staticmethod
    def get_guc_value_4_max_process_memory_balanced(guc_dict, cluster_info, min_value):
        """
        calculate value for guc: max_process_memory_balanced
        """
        guc = "max_process_memory_balanced"
        min_physical_memory = min_value.get("min_physical_memory")
        if cluster_info.isHtapCluster():
            one_node_dn_num = 1
            num = 0
        else:
            one_node_dn_num = cluster_info.get_dn_num_in_one_node()
            total_data_node_num = cluster_info.get_total_data_node_dum()
            if int(total_data_node_num) < 256:
                num = 1
            elif int(total_data_node_num) < 512:
                num = 2
            else:
                num = 3

        if guc_dict.get(guc).find('RESERVE_MEMORY') > 0:
            # dn max_process_memory_balanced:
            # PHYSIC_MEMORY-RESERVE_MEMORY-CN_MAX_PROCESS_MEMORY/MAX_MASTER_DATANUM_IN_ONENODE
            reserve_memory = DynamicGucConfig.get_reserve_memory(cluster_info, min_physical_memory)
            cn_max_process_memory = DynamicGucConfig.get_cn_max_process_memory(min_physical_memory, one_node_dn_num)
            guc_dict[guc] = guc_dict.get(guc).replace("PHYSIC_MEMORY", str(min_physical_memory))
            guc_dict[guc] = guc_dict.get(guc).replace("RESERVE_MEMORY", str(reserve_memory))
            guc_dict[guc] = guc_dict.get(guc).replace("CN_MAX_PROCESS_MEMORY", str(cn_max_process_memory))
            guc_dict[guc] = guc_dict.get(guc).replace("MAX_MASTER_DATANUM_IN_ONENODE", str(one_node_dn_num))
        else:
            # cn max_process_memory_balanced: PHYSIC_MEMORY*0.4/(N+MAX_MASTER_DATANUM_IN_ONENODE)
            guc_dict[guc] = guc_dict.get(guc).replace("PHYSIC_MEMORY", str(min_physical_memory))
            guc_dict[guc] = guc_dict.get(guc).replace("MAX_MASTER_DATANUM_IN_ONENODE", str(one_node_dn_num))
            guc_dict[guc] = guc_dict.get(guc).replace("N", str(num))

        guc_dict[guc] = round(eval(guc_dict.get(guc)))
        guc_dict[guc] = int(guc_dict.get(guc))
        if 2 <= guc_dict[guc] <= 2047:
            guc_dict[guc] = str(guc_dict.get(guc)) + "GB"
        elif guc_dict[guc] < 2:
            guc_dict[guc] = "2GB"
        else:
            guc_dict[guc] = "2047GB"

    @staticmethod
    def get_guc_value_4_max_process_memory(guc_dict, cluster_info, min_value):
        """
        calculate value for guc: max_process_memory
        """
        guc = "max_process_memory"
        total_data_node_num = cluster_info.get_total_data_node_dum()
        min_physical_memory = min_value.get("min_physical_memory")
        if cluster_info.isHtapCluster():
            guc_dict[guc] = guc_dict.get(guc).replace("PHYSIC_MEMORY", str(min_physical_memory))
            guc_dict[guc] = guc_dict.get(guc).replace("MAX_MASTER_DATANUM_IN_ONENODE", "0")
            guc_dict[guc] = guc_dict.get(guc).replace("N", "1")
        else:
            one_node_dn_num = cluster_info.get_dn_num_in_one_node()
            guc_dict[guc] = guc_dict.get(guc).replace("MAX_MASTER_DATANUM_IN_ONENODE", str(one_node_dn_num))
            if one_node_dn_num == 1:
                # if cluster has one master DN, max_process_memory = physic_memo * 0.8 * 3 / 4 / 1
                guc_dict[guc] = guc_dict.get(guc).replace("PHYSIC_MEMORY", str(min_physical_memory * 3 / 4))
                guc_dict[guc] = guc_dict.get(guc).replace("N", "0")
            else:
                # if cluster has more than one master DN, max_process_memory = physic_memo * 0.8 / (1 + DN)
                guc_dict[guc] = guc_dict.get(guc).replace("PHYSIC_MEMORY", str(min_physical_memory))
                if int(total_data_node_num) < 256:
                    num = 1
                elif int(total_data_node_num) < 512:
                    num = 2
                else:
                    num = 3
                guc_dict[guc] = guc_dict.get(guc).replace("N", str(num))
        guc_dict[guc] = round(eval(guc_dict.get(guc)))
        guc_dict[guc] = int(guc_dict.get(guc))
        if 2 <= guc_dict[guc] <= 2047:
            guc_dict[guc] = str(guc_dict.get(guc)) + "GB"
        elif guc_dict[guc] < 2:
            guc_dict[guc] = "2GB"
        else:
            guc_dict[guc] = "2047GB"

    @staticmethod
    def get_dynamic_guc_value(min_value, clusterInfo, guc_dict):
        """
        get smp guc value: comm_max_stream, max_connections
        value:
        comm_max_stream: min(int(query_dop_limit * query_dop_limit * 2 * 20),
                   int(max_process_memory * 0.025 // (max_coordinators + comm_max_datanode) // 260))
        query_dop_limit: int(min_cpu_num // one_node_dn_num)
        max_connections: int(query_dop_limit * 20 * 6 + 24)
        """
        min_cpu_num = min_value.get("min_cpu_info")
        one_node_dn_num = clusterInfo.get_dn_num_in_one_node()
        query_dop_limit = int(min_cpu_num // one_node_dn_num)
        comm_max_stream = DynamicGucConfig.get_query_dop_limit(
            DynamicGucConfig.do_unit_conversion(guc_dict.get('max_process_memory'), default_unit='KB'),
            int(guc_dict.get('max_coordinators')),
            int(guc_dict.get('comm_max_datanode')),
            query_dop_limit)
        if comm_max_stream <= MIN_COMM_MAX_STREAM:
            comm_max_stream = MIN_COMM_MAX_STREAM
        elif comm_max_stream >= MAX_COMM_MAX_STREAM:
            comm_max_stream = MAX_COMM_MAX_STREAM
        guc_dict["comm_max_stream"] = guc_dict.get("comm_max_stream").replace("COMM_MAX_STREAM", str(comm_max_stream))
        max_connections = int(query_dop_limit * 20 * 6 + 24)
        if max_connections <= MIN_COMM_MAX_CONNECTIONS:
            max_connections = MIN_COMM_MAX_CONNECTIONS
        guc_dict['max_connections'] = guc_dict.get('max_connections').replace("MAX_CONNECTIONS", str(max_connections))
        guc_dict['max_pool_size'] = guc_dict.get('max_pool_size').replace("MAX_POOL_SIZE", str(max_connections))
        guc_dict['max_stream_pool'] = min(int(guc_dict['max_connections']),
                                          int(DynamicGucConfig.do_unit_conversion(
                                              guc_dict.get('max_process_memory'),
                                              default_unit='KB') / 1024 / 1024 / 16 // 5),
                                          1024)
        return guc_dict

    @staticmethod
    def get_related_guc_value(guc_dict, guc):
        """
        """
        if guc in guc_dict.keys():
            guc_dict["comm_usable_memory"] = DynamicGucConfig.get_comm_usable_memory_value(
                guc_dict.get("max_process_memory"))
            guc_dict["udf_memory_limit"] = DynamicGucConfig.get_udf_memory_limit_value(
                guc_dict.get("max_process_memory"))
            guc_dict["shared_buffers"] = guc_dict.get("shared_buffers").replace(
                "SHARED_BUFFERS",
                DynamicGucConfig.get_shared_buffers_value(guc_dict.get("max_process_memory")))
            guc_dict["shared_buffers"] = str(round(eval(guc_dict.get("shared_buffers")))) + "MB"
            guc_dict["cstore_buffers"] = guc_dict.get("cstore_buffers").replace("CSTORE_BUFFERS",
                                                                                guc_dict.get("shared_buffers"))
            guc_dict["comm_memory_pool"] = DynamicGucConfig.get_comm_memory_pool_value(
                guc_dict.get("max_process_memory"))
            if int(guc_dict["max_process_memory"][0:-2]) < 30:
                guc_dict["work_mem"] = guc_dict.get("work_mem").replace("WORK_MEM", "512MB")
            else:
                guc_dict["work_mem"] = guc_dict.get("work_mem").replace("WORK_MEM", "2GB")
        return guc_dict

    @staticmethod
    def get_query_dop_limit(max_process_memory, max_coordinators, comm_max_datanode, query_dop_limit):
        """
        """
        return min(int(query_dop_limit * query_dop_limit * 2 * 20),
                   int(max_process_memory * 0.025 // (max_coordinators + comm_max_datanode) // 260))

    @staticmethod
    def do_unit_conversion(value, default_unit=""):
        """
        return: unit bytes
        """
        val = str(value).upper().replace("'", "").replace('"', '').strip()
        if default_unit not in ("KB", "8KB", ""):
            raise Exception(ErrorCode.GAUSS_500["GAUSS_50004"] % 'the default unit of guc parameter')
        if val.endswith("KB"):
            return int("".join(list(filter(str.isdigit, val)))) * 1024
        elif val.endswith("MB"):
            return int("".join(list(filter(str.isdigit, val)))) * 1024 * 1024
        elif val.endswith("GB"):
            return int("".join(list(filter(str.isdigit, val)))) * 1024 * 1024 * 1024
        elif val.endswith("TB"):
            return int("".join(list(filter(str.isdigit, val)))) * 1024 * 1024 * 1024 * 1024
        elif default_unit == "KB":
            return int("".join(list(filter(str.isdigit, val)))) * 1024
        elif default_unit == "8KB":
            return int("".join(list(filter(str.isdigit, val)))) * 8 * 1024
        else:
            return int("".join(list(filter(str.isdigit, val))))

    @staticmethod
    def get_comm_usable_memory_value(max_process_memory_value_str):
        """
        get comm_usable_memory
        value: max_process_memory / 8
        """
        max_process_memory_value = DynamicGucConfig.do_unit_conversion(
            max_process_memory_value_str, default_unit='KB') / 1024 / 1024 // 1024
        if 8 < max_process_memory_value <= 2047:
            return "%dGB" % round(max_process_memory_value / 8)
        elif max_process_memory_value <= 8:
            return "1GB"
        else:
            return "256GB"

    @staticmethod
    def get_comm_memory_pool_value(max_process_memory_value_str):
        """
        get comm_memory_pool
        value: comm_usable_memory * 0.5
        """
        comm_usable_memory = DynamicGucConfig.get_comm_usable_memory_value(max_process_memory_value_str)
        if comm_usable_memory == "1GB":
            comm_memory_pool = "512MB"
        else:
            comm_memory_pool = str(round(0.5 * int(comm_usable_memory.split("GB")[0]))) + "GB"
        return comm_memory_pool

    @staticmethod
    def get_udf_memory_limit_value(max_process_memory_value_str):
        """
        get udf_memory_limit
        value: max_process_memory / 20
        """
        max_process_memory_value = DynamicGucConfig.do_unit_conversion(
            max_process_memory_value_str, default_unit='KB') / 1024 / 1024 // 1024
        if 4 <= max_process_memory_value <= 2047:
            udf_memory_limit_value = max_process_memory_value / 20
            if udf_memory_limit_value < 1:
                return "%dMB" % round(udf_memory_limit_value * 1024)
            else:
                return "%dGB" % round(udf_memory_limit_value)
        elif max_process_memory_value < 4:
            return "200MB"
        else:
            return "102GB"

    @staticmethod
    def get_shared_buffers_value(max_process_memory_value_str):
        """
        get shared_buffers
        value: pow(2, round(math.log(max_process_memory_value // 18, 2), 0))
        """
        max_process_memory_value = DynamicGucConfig.do_unit_conversion(
            max_process_memory_value_str, default_unit='KB') / 1024 // 1024
        shared_buffers_value = pow(2, round(math.log(max_process_memory_value // 18, 2), 0))
        return str(shared_buffers_value)
