# -*- 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 getDictKeyByValue(ditKey, distMap):
        for realKey, values in distMap.items():
            if ditKey in values:
                return realKey
            
        return False

    @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, useThreadPool = True):
        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)
        begin = datetime.datetime.now()

        # 同步执行：逐个调用 mExec，并手动处理结果（替代线程回调）
        if useThreadPool is False:
            for param in paramList:
                ret = CliTool.doExec(param[1])
                # 手动执行原 threadCallback 的逻辑
                CliTool.log(ret['result'])
                if (len(ret['error']) <= 0) and (
                    ('building file list ... done' in ret['result']) or 
                    ('Total bytes received' in ret['result'])
                ):
                    CliTool.successIps[ret['hostname']] = ret['ip']
                else:
                    CliTool.failIps[ret['hostname']] = ret['ip']
                CliTool.hostUseTimeMap[ret['hostname']] = ret['useTime']
        else:
            pool = ThreadPool(50)
            requests = makeRequests(CliTool.mExec, paramList, CliTool.threadCallback)
            [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_ip = random.choice(list(ips.values()))

            # 获取对应的 module 值
            module_value = random_ip['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") + "\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 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=__pycache__ --exclude=.qoder --exclude=.trae --exclude=.codebuddy --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

    @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)  # 如果命令失败，退出程序

    @staticmethod
    def checkGitSubmodules(realLocalSrc):
        """
        检查git仓库的子模块是否是最新的远程节点

        Args:
            realLocalSrc: git仓库路径

        Returns:
            tuple: (is_valid, outdated_submodules, has_submodules)
                is_valid: bool, 是否所有子模块都是最新的（或不存在子模块）
                outdated_submodules: list, 过期的子模块列表
                has_submodules: bool, 是否存在子模块
        """
        api_git_path = os.path.join(realLocalSrc, '.git')
        if not os.path.exists(api_git_path):
            return True, [], False

        # 检查是否存在.gitmodules文件
        gitmodules_path = os.path.join(realLocalSrc, '.gitmodules')
        if not os.path.exists(gitmodules_path):
            return True, [], False

        # 获取子模块状态
        submodules_res = os.popen("cd " + realLocalSrc + " && git submodule status")
        submodules_output = submodules_res.read().strip()

        if not submodules_output:
            return True, [], False

        # 检查每个子模块是否是最新远程节点
        submodule_lines = submodules_output.split('\n')
        outdated_submodules = []

        for line in submodule_lines:
            if not line.strip():
                continue
            # 解析子模块状态: commit hash, path, version
            parts = line.strip().split()
            if len(parts) >= 2:
                current_commit = parts[0].lstrip('-+U')  # 移除状态前缀
                submodule_path = parts[1]
                full_submodule_path = os.path.join(realLocalSrc, submodule_path)

                if os.path.exists(full_submodule_path):
                    # 获取子模块的远程最新commit（先尝试origin/master，再尝试origin/main）
                    remote_res = os.popen("cd " + full_submodule_path + " && git rev-parse --verify origin/master 2>/dev/null || git rev-parse --verify origin/main 2>/dev/null || git rev-parse HEAD")
                    remote_commit = remote_res.read().strip()

                    if current_commit != remote_commit:
                        outdated_submodules.append({
                            'path': submodule_path,
                            'current': current_commit[:8],
                            'remote': remote_commit[:8]
                        })

        return len(outdated_submodules) == 0, outdated_submodules, True

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-s12')
    print(hostname)
