#!/usr/bin/env python3
# -*- coding:utf-8 -*-
try:
    import stat
    import sys
    import os
    import subprocess
    import re
    import time

    sys.path.append(sys.path[0] + "/../../../")
    from gspylib.common.ErrorCode import ErrorCode
    from gspylib.component.BaseComponent import BaseComponent
    from gspylib.os.gsfile import g_file, g_Platform
    from gspylib.common.Common import DefaultValue, ClusterInstanceConfig
except ImportError as ie:
    sys.exit("[GAUSS-52200] : Unable to import module: %s." % str(ie))

MAX_PARA_NUMBER = 500


class Kernel(BaseComponent):
    '''
    The class is used to define base component.
    '''

    # Desc:
    # start/stop/query single instance

    def start(self, nodeType=""):
        """
        """
        instype = "restoremode"
        if (nodeType == "coordinator"):
            instype = "coordinator"
        elif (nodeType == "datanode"):
            instype = "datanode"
        elif (nodeType == "single"):
            instype = "single_node"

        cmd = "%s/gs_ctl start -D %s -Z %s -t %d" % (
            self.binPath, self.instInfo.datadir, instype, DefaultValue.TIMEOUT_GS_CTL_START)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            raise Exception(ErrorCode.GAUSS_516["GAUSS_51607"] % "instance" + " Error: \n%s." % output)

    def stop(self, nodeType="", stopMode=""):
        """
        """
        instType = "restoremode"
        if (nodeType == "coordinator"):
            instType = "coordinator"
        elif (nodeType == "datanode"):
            instType = "datanode"
        elif (nodeType == "single"):
            instType = "single_node"

        cmd = "%s/gs_ctl stop -D %s -Z %s -t %s " % (
            self.binPath, self.instInfo.datadir, instType, DefaultValue.TIMEOUT_GS_CTL_STOP)
        # check stop mode
        if stopMode != "":
            cmd += " -m %s" % stopMode
        status, output = subprocess.getstatusoutput(cmd)
        if status != 0:
            cmd = "%s/gs_ctl stop -D %s -Z %s -t %s -m i " % (
                self.binPath, self.instInfo.datadir, instType, DefaultValue.TIMEOUT_GS_CTL_STOP)
            status, output = subprocess.getstatusoutput(cmd)
            if status != 0:
                raise Exception(ErrorCode.GAUSS_516["GAUSS_51610"] % "instance" + " Error: \n%s." % output)

    def query(self):
        """
        """
        cmd = "%s/gs_ctl query -D %s" % (self.binPath, self.instInfo.datadir)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd + " Error: \n%s " % output)
        return (status, output)

    def queryBuild(self):
        """
        """
        cmd = "%s/gs_ctl querybuild -D %s" % (self.binPath, self.instInfo.datadir)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd + " Error: \n%s " % output)

    # Desc:
    # Under the AP branch, the installation package of each component is not distinguished.
    # After checking, unzip the public installation package and complete the installation.

    def install(self, nodeName="", dbInitParams=""):
        """
        """
        return None

    def getInstanceTblspcDirs(self, nodeName=""):
        """
        function: Get instance dirs
        input : NA
        output: NA
        """
        tbsDirList = []

        if (not os.path.exists("%s/pg_tblspc" % self.instInfo.datadir)):
            self.logger.debug("%s/pg_tblspc does not exists." % self.instInfo.datadir)
            return tbsDirList

        fileList = os.listdir("%s/pg_tblspc" % self.instInfo.datadir)
        if (len(fileList)):
            for filename in fileList:
                if (os.path.islink("%s/pg_tblspc/%s" % (self.instInfo.datadir, filename))):
                    linkDir = os.readlink("%s/pg_tblspc/%s" % (self.instInfo.datadir, filename))
                    if (os.path.isdir(linkDir)):
                        tblspcDir = "%s/%s_%s" % (linkDir, DefaultValue.TABLESPACE_VERSION_DIRECTORY, nodeName)
                        self.logger.debug("Table space directories is %s." % tblspcDir)
                        tbsDirList.append(tblspcDir)
                    else:
                        self.logger.debug("%s is not link directory." % linkDir)
                else:
                    self.logger.debug("%s is not a link file." % filename)
        else:
            self.logger.debug("%s/pg_tblspc is empty." % self.instInfo.datadir)

        return tbsDirList

    def getLockFiles(self):
        """
        function: Get lock files
        input : NA
        output: NA
        """
        fileList = []
        # the static file must be exists
        tmpDir = os.path.realpath(DefaultValue.getTmpDirFromEnv())

        pgsql = ".s.PGSQL.%d" % self.instInfo.port
        pgsqlLock = ".s.PGSQL.%d.lock" % self.instInfo.port
        fileList.append(os.path.join(tmpDir, pgsql))
        fileList.append(os.path.join(tmpDir, pgsqlLock))
        return fileList

    def removeSocketFile(self, fileName):
        """
        """
        g_file.removeFile(fileName, "shell")

    def removeTbsDir(self, tbsDir):
        """
        """
        g_file.removeDirectory(tbsDir)

    def cleanPgLocationDir(self, pglDir, resultMount):
        """
        function: Clean the dirs.
        input : pglDir, resultMount
        output: NA
        """
        cmd = "cd '%s'" % pglDir
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd +
                            " Error: \n%s " % output)

        outputFile = g_file.findFile(".", "f", "type")
        if (len(outputFile) > 0):
            for infoFile in outputFile:
                tmpinfoFile = pglDir + infoFile[1:]
                for infoMount in resultMount:
                    if (tmpinfoFile.find(infoMount) < 0 and
                            infoMount.find(tmpinfoFile) < 0):
                        realFile = "'%s/%s'" % (pglDir, infoFile)
                        g_file.removeFile(realFile, "shell")

        # delete directory in the pg_location directory.
        cmd = "if [ -d '%s' ]; then cd '%s' && find -type d; fi" % \
              (pglDir, pglDir)
        (status, outputDir) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            raise Exception(ErrorCode.GAUSS_502["GAUSS_50207"] % pglDir +
                            " Error:\n%s" % str(outputDir))
        else:
            resultDir = g_file.findFile(".", "d", "type")
            resultDir.remove(".")
            if (len(resultDir) > 0):
                for infoDir in resultDir:
                    tmpinfoDir = pglDir + infoDir[1:]
                    for infoMount in resultMount:
                        if (tmpinfoDir.find(infoMount) < 0 and
                                infoMount.find(tmpinfoDir) < 0):
                            realPath = "'%s/%s'" % (pglDir, infoDir)
                            g_file.removeDirectory(realPath)

    def cleanDir(self, instDir):
        """
        function: Clean the dirs
        input : NA
        output: NA
        """
        if (not os.path.exists(instDir)):
            return

        dataDir = os.listdir(instDir)
        if (os.getuid() == 0):
            pglDir = '%s/pg_location' % instDir
            isPglDirEmpty = False
            if (os.path.exists(pglDir) and len(os.listdir(pglDir)) == 0):
                isPglDirEmpty = True
            if (len(dataDir) == 0 or isPglDirEmpty):
                g_file.cleanDirectoryContent(instDir)
        else:
            for info in dataDir:
                if (str(info) == "pg_location"):
                    pglDir = '%s/pg_location' % instDir
                    # delete all files in the mount point
                    cmd = "%s | %s '%s' | %s '{printf $3}'" % \
                          (g_Platform.getMountCmd(), g_Platform.getGrepCmd(),
                           pglDir, g_Platform.getAwkCmd())
                    (status, outputMount) = subprocess.getstatusoutput(cmd)
                    if (status != 0):
                        raise Exception(ErrorCode.GAUSS_502["GAUSS_50207"] % instDir +
                                        " Error:\n%s" % str(outputMount))
                    else:
                        if (len(outputMount) > 0):
                            resultMount = str(outputMount).split()
                            for infoMount in resultMount:
                                g_file.cleanDirectoryContent(infoMount)
                        else:
                            g_file.cleanDirectoryContent(instDir)
                            continue

                    # delete file in the pg_location directory
                    if (not os.path.exists(pglDir)):
                        continue
                    self.cleanPgLocationDir(pglDir, resultMount)

            cmd = "if [ -d '%s' ];then cd '%s' && find . ! -name 'pg_location' ! " \
                  "-name '..' ! -name '.' -print0 |xargs -r -0 -n100 rm -rf; fi" % \
                  (instDir, instDir)
            (status, output) = subprocess.getstatusoutput(cmd)
            if (status != 0):
                raise Exception(ErrorCode.GAUSS_502["GAUSS_50207"] % instDir + " Error:\n%s" % str(output))

    def uninstall(self, instNodeName):
        """
        function: Clean node instances.
                  1.get the data dirs, tablespaces, soketfiles
                  2.use theard delete the dirs or files
        input : NA
        output: NA
        """
        self.logger.log("Cleaning instance.")

        # clean disk cache directory
        self.clean_disk_cache_base_paths()

        # tablespace data directory
        tbsDirList = self.getInstanceTblspcDirs(instNodeName)

        # sockete file
        socketFiles = self.getLockFiles()

        # clean tablespace dir
        if (len(tbsDirList) != 0):
            try:
                self.logger.debug("Deleting instances tablespace directories.")
                for tbsDir in tbsDirList:
                    self.removeTbsDir(tbsDir)
            except Exception as e:
                raise Exception(str(e))
            self.logger.log("Successfully cleaned instance tablespace.")

        if (len(self.instInfo.datadir) != 0):
            try:
                self.logger.debug("Deleting instances directories.")
                self.cleanDir(self.instInfo.datadir)
            except Exception as e:
                raise Exception(str(e))
            self.logger.log("Successfully cleaned instances.")

        if (len(socketFiles) != 0):
            try:
                self.logger.debug("Deleting socket files.")
                for socketFile in socketFiles:
                    self.removeSocketFile(socketFile)
            except Exception as e:
                raise Exception(str(e))
            self.logger.log("Successfully cleaned socket files.")

    def clean_disk_cache_base_paths(self):
        disk_cache_base_paths = self.get_disk_cache_base_paths()
        if disk_cache_base_paths:
            disk_cache_dir_list = disk_cache_base_paths.split(" ")
            for disk_cache_dir in disk_cache_dir_list:
                basedir = os.path.dirname(disk_cache_dir)
                if basedir == self.instInfo.datadir:
                    continue
                if not os.path.exists(disk_cache_dir):
                    self.logger.debug("The disk cache directory not exists: %s" % disk_cache_dir)
                    continue
                try:
                    self.logger.debug("Deleting disk cache directory: %s" % disk_cache_dir)
                    self.cleanDir(disk_cache_dir)
                    g_file.removeDirectory(disk_cache_dir)
                except Exception as e:
                    raise Exception(str(e))
            self.logger.log("Successfully disk cache directory.")

    def get_disk_cache_base_paths(self):
        pg_conf_file = os.path.join(self.instInfo.datadir, "postgresql.conf")
        if not os.path.isfile(pg_conf_file):
            self.logger.debug("The config file [%s] not exists" % pg_conf_file)
            return ""
        cmd = "grep -wi '^\\s*disk_cache_base_paths.*=.*$' %s" % pg_conf_file
        status, output = subprocess.getstatusoutput(cmd)
        if status != 0:
            self.logger.debug("The disk_cache_base_paths from file [%s] cannot be found." % pg_conf_file)
            return ""
        disk_cache_base_paths = output.split("=")[1].strip().replace('"', '').replace("'", "")
        pos = disk_cache_base_paths.find(' #')
        if pos >= 0:
            disk_cache_base_paths = disk_cache_base_paths[:pos]
        self.logger.debug("The disk_cache_base_paths value of datadir [%s]: %s" % (pg_conf_file, disk_cache_base_paths))
        return disk_cache_base_paths

    def setCommonItems(self):
        """
        function: set common items
        input : tmpDir
        output: tempCommonDict
        """
        tempCommonDict = {}
        tmpDir = DefaultValue.getTmpDirFromEnv()
        tempCommonDict["unix_socket_directory"] = "'%s'" % tmpDir
        tempCommonDict["unix_socket_permissions"] = "0700"
        tempCommonDict["log_file_mode"] = "0600"
        tempCommonDict["max_datanodes"] = "4096"
        tempCommonDict["enable_nestloop"] = "off"
        tempCommonDict["enable_mergejoin"] = "off"
        tempCommonDict["explain_perf_mode"] = "pretty"
        tempCommonDict["log_line_prefix"] = "'%m %c %d %u %p %a %x %n %e '"

        return tempCommonDict

    def doGUCConfig(self, action, GUCParasStr, isHab=False):
        """
        """
        # check instance data directory
        if self.instInfo.datadir == "" or not os.path.exists(self.instInfo.datadir):
            raise Exception(ErrorCode.GAUSS_502["GAUSS_50219"] %
                            ("data directory of the instance[%s]" % str(self.instInfo)))

        if GUCParasStr == "":
            return

        # check conf file
        if isHab:
            configFile = "%s/pg_hba.conf" % self.instInfo.datadir
        else:
            configFile = "%s/postgresql.conf" % self.instInfo.datadir
        if not os.path.exists(configFile):
            raise Exception(ErrorCode.GAUSS_502["GAUSS_50201"] % configFile)

        if self.instInfo.instanceRole == DefaultValue.INSTANCE_ROLE_COODINATOR:
            instype = "coordinator"
        else:
            instype = "datanode"
        cmd = "%s/gs_guc %s -Z %s -D %s %s " % (self.binPath, action, instype, self.instInfo.datadir, GUCParasStr)
        (status, output) = DefaultValue.retryGetstatusoutput(cmd, 3, 3)
        self.logger.debug("Setting guc command is %s." % cmd)
        if status != 0:
            raise Exception(ErrorCode.GAUSS_500["GAUSS_50007"] % "GUC" + " Command: %s. Error:\n%s" % (cmd, output))

    def setGucConfig(self, paraDict=None, setMode='set'):
        """
        """
        i = 0
        GUCParasStr = ""
        para_list = []
        GUCParasStrList = []
        for paras in paraDict:
            i += 1
            para_list.append(" -c \"%s=%s\" " % (paras, paraDict[paras]))
            GUCParasStr = "".join(para_list)
            if i % MAX_PARA_NUMBER == 0:
                GUCParasStrList.append(GUCParasStr)
                i = 0
                GUCParasStr = ""
                para_list = []
        if GUCParasStr:
            GUCParasStrList.append(GUCParasStr)

        for parasStr in GUCParasStrList:
            self.doGUCConfig(setMode, parasStr, False)

    def removeIpInfoOnPghbaConfig(self, ipAddressList):
        """
        """
        i = 0
        GUCParasStr = ""
        GUCParasStrList = []
        para_list = []
        for ipAddress in ipAddressList:
            i += 1
            para_list.append(" -h \"%s\"" % ClusterInstanceConfig.getHbaItemForIP(ipAddress, ""))
            GUCParasStr = "".join(para_list)
            if i % MAX_PARA_NUMBER == 0:
                GUCParasStrList.append(GUCParasStr)
                i = 0
                GUCParasStr = ""
                para_list = []
        if GUCParasStr:
            GUCParasStrList.append(GUCParasStr)

        for parasStr in GUCParasStrList:
            self.doGUCConfig("set", parasStr, True)

    def getSQLCommand(self, port, database=DefaultValue.DEFAULT_DB_NAME, gsqlBin="gsql", application_name="OM"):
        """
        function : get SQL command
        input : port, database
        output : cmd
        """
        cmd = DefaultValue.SQL_EXEC_COMMAND_WITHOUT_USER_FOR_UPGRADE % (gsqlBin, port, database, application_name)
        return cmd

    def findErrorInSqlFile(self, sqlFile, output):
        """
        function : Find error in the sql file
        """
        GSQL_BIN_FILE = "gsql"
        # init flag
        ERROR_MSG_FLAG = "(ERROR|FATAL|PANIC)"
        GSQL_ERROR_PATTERN = "^%s:%s:(\d*): %s:.*" % (GSQL_BIN_FILE, sqlFile, ERROR_MSG_FLAG)
        pattern = re.compile(GSQL_ERROR_PATTERN)
        for line in output.split("\n"):
            line = line.strip()
            result = pattern.match(line)
            if result is not None:
                return True
        return False

    def findErrorInSqlQuery(self, output):
        """
        function : Find error in sql
        """
        # init flag
        ERROR_MSG_FLAG = "(ERROR|FATAL|PANIC)"
        ERROR_PATTERN = "^%s:.*" % ERROR_MSG_FLAG
        pattern = re.compile(ERROR_PATTERN)

        for line in output.split("\n"):
            line = line.strip()
            result = pattern.match(line)
            if result is not None:
                return True
        return False

    def execSQLFile(self, sqlFile, user, host, port, database="postgres", option=""):
        """
        function : Execute sql file
        input : String,String,String,int
        output : String
        """
        database = database.replace('$', '\$')
        currentTime = time.strftime("%Y-%m-%d_%H%M%S")
        pid = os.getpid()
        # init SQL result file
        queryResultFile = os.path.join(DefaultValue.getTmpDirFromEnv(user),
                                       "gaussdb_result.sql_%s_%s_%s" % (str(port), str(currentTime), str(pid)))
        if (os.path.exists(queryResultFile)):
            DefaultValue.cleanFile(queryResultFile)

        # init hostPara
        userProfile = DefaultValue.getMpprcFile()
        hostPara = ("-h %s" % host) if host != "" else ""
        # build shell command
        # if the user is root, switch the user to execute
        gsql_cmd = self.getSQLCommand(port, database)
        if os.getuid() == 0:
            cmd = "su - %s -c \"source %s; %s %s -f '%s' --output '%s' -t -A -X %s\" \
            " % (user, userProfile, gsql_cmd, hostPara, sqlFile, queryResultFile, option)
        else:
            cmd = "%s %s -f '%s' --output '%s' -t -A -X %s" % (gsql_cmd, hostPara, sqlFile, queryResultFile, option)
        (status, output) = subprocess.getstatusoutput(cmd)
        if self.findErrorInSqlFile(sqlFile, output):
            status = 1
        if status != 0:
            DefaultValue.cleanFile(queryResultFile)
            return (status, output)

        # read the content of query result file.
        try:
            with open(queryResultFile, 'r') as fp:
                rowList = fp.readlines()
        except Exception as e:
            DefaultValue.cleanFile(queryResultFile)
            return 1, str(e)

        if os.path.exists(queryResultFile):
            DefaultValue.cleanFile(queryResultFile)

        return 0, "".join(rowList)[:-1]

    def execSQLCommand(self, sql, user, host, port, database="postgres", option="", retry_time=3):
        """
        function : Execute sql command
        input : String,String,String,int
        output : String
        """
        database = database.replace('$', '\$')
        currentTime = time.strftime("%Y-%m-%d_%H%M%S")
        pid = os.getpid()
        # init SQL query file
        sqlFile = os.path.join(DefaultValue.getTmpDirFromEnv(user),
                               "gaussdb_query.sql_%s_%s_%s" % (str(port), str(currentTime), str(pid)))

        if self.dwsMode:
            sql = "set cgroup_name='Rush';" + sql
        # write the SQL command into sql query file
        try:
            g_file.write_file_with_default_permission(sqlFile, sql)
        except Exception as e:
            DefaultValue.cleanFile(sqlFile)
            return 1, str(e)
        try:
            count = 0
            while True:
                (status, output) = self.execSQLFile(sqlFile, user, host, port, database, option)
                if status == 0:
                    break
                if count < retry_time:
                    self.logger.debug("Warning: Sql execute failed [%s]. Output:%s" % (sql, output))
                count += 1
                time.sleep(3)
                if count > 3:
                    break
            DefaultValue.cleanFile(sqlFile)
            return status, output
        except Exception as e:
            DefaultValue.cleanFile(sqlFile)
            return 1, str(e)

    def killProcess(self, signaltype, pidList):
        """
        function: kill process
        input:  process flag
        output: NA
        """
        killCmd = g_Platform.getKillProcessCmd(signaltype, " ".join(pidList))
        self.logger.debug("Command for killing: %s" % killCmd)
        (status, output) = subprocess.getstatusoutput(killCmd)
        if status != 0:
            raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % killCmd + "\nOutput:%s" % output)

    def getAllDatabase(self):
        """
        function: Get all the databases in the cluster
        output: databaseList
        """
        self.logger.log("Getting all the databases.")
        try:
            # get databases from cluster
            selectSql = "SELECT datname FROM pg_catalog.pg_database WHERE datallowconn ORDER BY 1"
            self.logger.debug("Get databases from cluster command: %s." % selectSql)
            (status, output) = self.execSQLCommand(selectSql, self.user, "", self.instInfo.port)
            if status != 0:
                raise Exception(ErrorCode.GAUSS_513["GAUSS_51300"] % selectSql + " Error: \n%s" % str(output))
            self.logger.debug("Get databases from cluster result: %s." % output)
            # split the output string with '\n'
            databaseList = output.split("\n")
            return databaseList
        except Exception as e:
            raise Exception(str(e))

    def cleanConnection(self, sql):
        """
        Clean the connection with CN nodes
        """
        # Clean connection for "template1" database
        self.logger.debug("Get the connection cleaned command: %s" % sql)
        # Execute clean command
        (status, output) = self.execSQLCommand(sql, self.user, "", self.instInfo.port)
        if status != 0:
            raise Exception(ErrorCode.GAUSS_513["GAUSS_51300"] % sql + " Error: \n%s" % str(output))
        else:
            self.logger.debug("Get the connection cleaned result: %s" % output)
