# -*- coding:utf-8 -*-
import subprocess, os, time, codecs, sys, re, datetime, random
from threadpool import makeRequests, ThreadPool
import requests
import json

class CliTool(object):
    successIps = {}
    failIps = {}
    hostUseTimeMap = {}

    @staticmethod
    def getIpsMap(filePath, isIntra=True, defaultPort = 8873, defaultModule = '', forePort = False):
        ipsMap = {}
        with open(filePath, 'r', encoding='utf-8', newline='') as f:
            for line in f:
                if len(line.strip()) <= 0:
                    continue
                if line.startswith('#'):
                    continue
                host_arr = line.split('|')
                if len(host_arr) < 5:
                    continue

                host_arr = [one.strip() for one in host_arr]

                port = defaultPort
                protocol = 'rsync'
                if len(host_arr) == 5:
                    env, module, host, ip, intraIp = host_arr
                if len(host_arr) == 6:
                    env, module, host, ip, intraIp, port = host_arr
                if len(host_arr) == 7:
                    env, module, host, ip, intraIp, port, protocol = host_arr

                if defaultModule != '':
                    module = defaultModule

                if (forePort is not None and forePort > 0) and defaultPort > 0:
                    port = forePort

                uniqueKey = '%s_%s' % (env, host)

                if isIntra is True:
                    targetIp = intraIp
                else:
                    targetIp = ip
                ecs = {'ip': targetIp, 'port': port, 'module': module, 'protocol': protocol, 'hostname': host}

                if env not in ipsMap.keys():
                    ipsMap[env] = {}

                if uniqueKey not in ipsMap[env].keys():
                    ipsMap[env][uniqueKey] = []

                ipsMap[env][uniqueKey] = ecs

        return ipsMap

    @staticmethod
    def getEnvList(envs, ipsMap, isIntra):
        envList = envs.split(',')

        if envs == 'all':
            envList = list(ipsMap.keys())

        # 如果是内网发布的，不要再发布一遍到发布机了
        if isIntra is True:
            for env in envList:
                if env == 'pub':
                    envList.remove(env)

                if env == 'pre2':
                    envList.remove(env)
        return envList

    def publish_one(self, ecs, mode, cmdTpl, isIntra = None, sshCmdTpl = ''):
        option = ('cvzrDp' if mode == 'real' else 'cvzrDpn')
        realCmdTpl = cmdTpl
        ip = ecs['ip']
        port = ecs['port']
        module = ecs['module']
        protocol = ecs['protocol']

        if isIntra is False and protocol == 'ssh' and sshCmdTpl:
            realCmdTpl = sshCmdTpl

        cmd = realCmdTpl % (option, port, ip, module)

        cmd = cmd.replace("[" + str(port) + "]", '')
        CliTool.log(cmd + ' ' + ecs['hostname'])
        param = {}
        param['hostname'] = CliTool.padHostname('%s' % ecs['hostname'])
        param['ip'] = ip
        param['cmd'] = cmd
        ret = CliTool.doExec(param)
        if (len(ret['error']) <= 0) and ((ret['result'].find('building file list ... done') != -1) or (
                ret['result'].find('Total bytes received') != -1)):
            return  {"result": "success", 'hostname': ecs['hostname'], "ip": ip}

        return {"result": "fail", 'hostname': ecs['hostname'], "ip": ip}

    def build_cmd_list(self, ips, mode, cmdTpl, isIntra = None, sshCmdTpl = ''):
        option = ('cvzrDp' if mode == 'real' else 'cvzrDpn')

        port = ''
        module = ''
        protocol = ''
        if mode == 'rdtest':
            host = random.choice(list(ips.keys()))
            ips = {host: ips[host]}
        paramList = []
        
        for key in ips:
            realCmdTpl = cmdTpl
            if isinstance(ips[key], dict):
                ip = ips[key]['ip']
                port = ips[key]['port']
                module = ips[key]['module']
                protocol = ips[key]['protocol']
            else:
                ip = key

            if isIntra is False and protocol == 'ssh' and sshCmdTpl:
                realCmdTpl = sshCmdTpl

            cmd = realCmdTpl % (option, port, ip, module)
            cmd = cmd.replace("[" + str(port) + "]", ''); 
            CliTool.log(cmd + ' ' + key)
            param = {}
            param['hostname'] = CliTool.padHostname(key)
            param['ip'] = ip
            param['cmd'] = cmd
            paramInfo = (None, param)
            paramList.append(paramInfo)
        
        return paramList
    
    def checkPhpScriptSyntaxErrors(self, rsyncCmd):
        # 解析rsync输出，获取php的文件列表
        local_script_dir = rsyncCmd.split(" ")[-2]
        app_name = local_script_dir.split("/")[-2]

        rsync_result = self.execCommand(rsyncCmd)
        # 解析rsync输出，获取已删除的文件列表
        php_files = re.findall(r'^(?!deleting)(.+\.php)$', rsync_result, re.MULTILINE)
        if len(php_files) == 0:
            print("未检出到php文件， 继续发布")
            return False

        # 遍历PHP文件并检查语法错误
        error_mgs = ''
        for php_file in php_files:
            if php_file.startswith('api/vendor/') or php_file.startswith('vendor/'):
                continue

            php_script_path = os.path.join(local_script_dir, php_file)

            cmd = 'php -l {}'.format(php_script_path)
            result = self.execCommand(cmd)
            if 'No syntax errors' in result:
                continue

            result = result.replace(local_script_dir, "")
            error_mgs += result + '\n'

        if error_mgs == '':
            return False
        
        ##self.sendErrorInfoByTingTalkRobots(app_name, error_mgs)
        self.sendErrorInfoByJopsRobots(app_name, error_mgs)
        return True

    @staticmethod
    def sendErrorInfoByTingTalkRobots(app_name, errorMsg):
        robots_url = 'https://oapi.dingtalk.com/robot/send?access_token=489a8cfd7aac5004426647ff66d3eaef52f24968fbc69bc11cc5b86468e8d196'
        data = {
            "msgtype": "text",
            "text": {
                "content": "项目[{}]发布时PHP语法错误： \n\n {} ".format(app_name, errorMsg)
            },
            "at": {
                "isAtAll": False
            }
        }
        
        json_data = json.dumps(data)
        response = requests.post(robots_url, data=json_data, headers={'Content-Type': 'application/json'})
        if response.status_code == 200:
            return True
        else:
            return False

    @staticmethod
    def sendErrorInfoByJopsRobots(app_name, errorMsg, msgType = 'php'):
        robots_url = 'http://jflow.jiancent.com/open/ding_talk/sendTextMsgToGroup'
        msgTypeName = 'PHP存在语法错误' if 'php' in msgType else '服务器发布异常'
        form_data = {
            'sign': '562830de9f752e8bff88685ab92351ed604f4077',
            'groupId': 'cidRi6GD08s2emMouu0SP2HTA==',
            'msg' : "项目[{}]发布时 {}： \n\n {} ".format(app_name, msgTypeName, errorMsg),
        }
        
        response = requests.post(robots_url, data=form_data)
        if response.status_code == 200:
            return True
        else:
            return False
        
    def publish(self, ips, mode, cmdTpl, isIntra = None, sshCmdTpl = '', php_syntax_check = False):
        if mode == 'real' and php_syntax_check is True:
            print("正在检查php文件的语法错误 ...")
            randList = self.build_cmd_list(ips, 'rdtest', cmdTpl, isIntra, sshCmdTpl)
            rsyncCmdMap=randList[0][1]
            chectRet = self.checkPhpScriptSyntaxErrors(rsyncCmdMap['cmd'])
            if chectRet == True:
                CliTool.log("---------发布异常，检查PHP是否存在语法错误，详见Ops群通知，联系开发---------")
                return 
        
        paramList = self.build_cmd_list(ips, mode, cmdTpl, isIntra, sshCmdTpl)
        pool = ThreadPool(50)
        requests = makeRequests(CliTool.mExec, paramList, CliTool.threadCallback)
        begin = datetime.datetime.now()
        [pool.putRequest(req) for req in requests]
        pool.wait()

        end = datetime.datetime.now()
        useTime = end - begin

        successIps = dict(sorted(self.successIps.items(), key=lambda x: x[0]))
        failIps = dict(sorted(self.failIps.items(), key=lambda x: x[0]))

        CliTool.log("-------------------------- " + mode + " Publish summary start ------------------------------")
        for key in successIps.keys():
            CliTool.log("{}\t{}\t{}\t{}\tsuccess".format(key, self.successIps[key], self.hostUseTimeMap[key], mode))

        # 失败日志
        fail_logs = []
        for ip, info in failIps.items():
            log_line = "%s\t%s\t%s\t%s\tfail" % (ip, info, self.hostUseTimeMap.get(ip, "-"), mode)
            CliTool.log(log_line)
            fail_logs.append("%s %s" % (time.strftime('%Y-%m-%d %H:%M:%S'), log_line))

        # --- Step 4: 错误通知 ---
        if fail_logs and mode == 'real':
            fail_output = "\r\n".join(fail_logs)
            # 随机选择一个键
            random_key = random.choice(list(failIps.keys()))

            # 获取对应的 module 值
            module_value = ips[random_key]['module']
            app_name = module_value.split("/")[-1]
            
            self.sendErrorInfoByJopsRobots(app_name, fail_output, 'ecs')

        CliTool.log("\n\t\t\t\t publish use time: %s" % useTime)
        CliTool.log("-------------------------- " + mode + " Publish summary end ------------------------------")

    @staticmethod
    def threadCallback(workRequest, ret):
        CliTool.log(ret['result'])
        if (len(ret['error']) <= 0) and ((ret['result'].find('building file list ... done') != -1) or (ret['result'].find('Total bytes received') != -1)):
            CliTool.successIps[ret['hostname']] = ret['ip']
        else:
            CliTool.failIps[ret['hostname']] = ret['ip']

        CliTool.hostUseTimeMap[ret['hostname']] = ret['useTime']

    @staticmethod
    def padHostname(hostname):
        # 匹配结尾的连续数字
        match = re.search(r'(\d+)$', hostname)
        if match:
            number_str = match.group(1)
            padded = number_str.zfill(3)  # 补零到3位
            # 替换原数字部分
            return hostname[:match.start(1)] + padded
        else:
            # 如果没有结尾数字，原样返回（根据你的数据应该不会出现）
            return hostname


    @staticmethod
    def mExec(**paramList):
        hostname = paramList['hostname']
        ip = paramList['ip']
        cmd = paramList['cmd']
        begin = datetime.datetime.now()

        #mock_result = {'result': 'building file list ... done  -- mock', 'error': 'xxx', 'hostname': hostname, 'ip': ip, 'useTime': 10}
        #return mock_result
    
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)

        result = ''
        for line in iter(p.stdout.readline, b''):
            result += line.rstrip().decode("utf-8") + "\n"

        p.wait()
        end = datetime.datetime.now()
        useTime = end - begin
        #result = p.stdout.read().decode('utf-8')
        # print(result)
        error = p.stderr.read().decode('utf-8')
        # print(error)
        return {'result': result, 'error': error, 'hostname': hostname, 'ip': ip, 'useTime': useTime}

    @staticmethod
    def doExec(param):
        if type(param) != dict:
            return  {'result': "fail", 'error': "param is not dict data"}

        hostname = param['hostname']
        ip = param['ip']
        cmd = param['cmd']
        begin = datetime.datetime.now()
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)

        result = ''
        for line in iter(p.stdout.readline, b''):
            result += line.rstrip().decode("utf-8")
    
        p.wait()
        end = datetime.datetime.now()
        useTime = end - begin
        #result = p.stdout.read().decode('utf-8')
        # print(result)
        error = p.stderr.read().decode('utf-8')
        # print(error)
        return {'result': result, 'error': error, 'hostname': hostname, 'ip': ip, 'useTime': useTime}

    @staticmethod
    def log(content):
        content = "%s %s\r\n" % (time.strftime('%Y-%m-%d %H:%M:%S'), content)
        sys.stdout.write(content)

        dir = os.getcwd() + '/log/'
        if not os.path.isdir(dir):
            os.makedirs(dir, 0o755)

        pLogFile = dir + 'publish.log.' + time.strftime('%Y-%m-%d_%H')
        with codecs.open(pLogFile, 'a', encoding='utf8') as f:
            f.write(content)

    @staticmethod
    def excludeFile():
        return " --exclude=.idea --exclude=.gitmodules --exclude=.git --exclude=.svn --exclude=.buildpath --exclude=.project --exclude=.settings --exclude=.DS_Store --exclude=.gitignore --exclude=/composer.* "

    @staticmethod
    def workspace():
        hostname = os.uname()[1]
        workspace = {
            'daxian-PC': '/data2/repo/zend_workspace/',
            'daxian-vm': '/home/daxain/zend_workspace/',
            'JX-YW-HYL': '/mnt/d/App.git/',
            'llh': '/Users/llh/PhpstormProjects/',
        }.get(hostname, '/Users/tangjianhui/Documents/zend_workspace/')

        return workspace
        #return '/data2/repo/zend_workspace/' if 'daxian' in hostname.lower() else '/Users/tangjianhui/Documents/zend_workspace/'

    @staticmethod
    def execCommand(cmd):
        outputStr = ''
        process = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        while process.poll() is None:
            line = process.stdout.readline()
            line = line.strip()
            if line:
                outputStr += line.decode('utf8', 'ignore') + '\n'
        return outputStr
    
    @staticmethod
    def processCmd(command):
        process = subprocess.Popen(
            command,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True
        )

        # 实时输出标准输出和标准错误
        for line in process.stdout:
            print(line.strip())

        for line in process.stderr:
            print(line.strip())

        # 等待子进程执行完毕
        process.wait()

        # 检查命令的返回码
        if process.returncode != 0:
            print("Command [{}] failed with return code {}".format(command, process.returncode))
            sys.exit(1)  # 如果命令失败，退出程序

    @staticmethod
    def processCmd2(command):
        outputStr = ''
        process = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        while process.poll() is None:
            line = process.stdout.readline()
            line = line.strip()
            if line:
                outputStr = line.decode('utf8', 'ignore') + '\n'
                print(outputStr)
                
        # 检查命令的返回码
        if process.returncode != 0:
            print("Command [{}] failed with return code {}".format(command, process.returncode))
            sys.exit(1)  # 如果命令失败，退出程序 
        
if __name__ == '__main__':
    # ipsMap = CliTool.getIpsMap('D:\publisher/hosts-honor.txt')
    # print(ipsMap)
    # print(ipsMap.keys())
    # print('----------------------------------------------------------------')
    # envList = CliTool.getEnvList('pub', ipsMap, True)
    # print(envList)
    # envList = CliTool.getEnvList('real', ipsMap, True)
    # print(envList)
    # envList = CliTool.getEnvList('all', ipsMap, True)
    # print(envList)
    # envList = CliTool.getEnvList('all', ipsMap, False)
    # print(envList)
    # print('---------------------------------------------------------------')
    # localPwdFile = '/home/porsche/publish/tjds1.passwd'
    # localSrc = '/alidata/www/honor/'
    #
    # cmdTpl = 'rsync -%s --delay-updates --del --stats --port=8873 --password-file=' + localPwdFile + ' --exclude=.gitmodules --exclude=.git --exclude=.svn --exclude=.buildpath --exclude=.project --exclude=.settings --exclude=.DS_Store --exclude=.gitignore '+localSrc+' www@%s::%s/'
    # ips = {}
    # for env in envList:
    #     ips = dict(ips, **ipsMap[env])
    # ct = CliTool()
    # ct.publish(ips, '', cmdTpl)
    hostname = CliTool.padHostname('honor-v2-s12')
    print(hostname)
