# -*- coding: utf-8 -*-
__author__ = 'hzp'

import os.path
import time
import semantic_version
import opcache_checker

from common import (
    exec_cmd,
    get_version,
    is_app_started,
    uclogger,
    BACKUP_PATH,
    set_elasticsearch55_resource
)


# 潜规则: 自动把这些组件的update_postscript转成update_start_postscript
UPDATE_START_POSTSCRIPT_COMPONENTS = ["cmdb_resource", "deploy", "tools"]


class Deployer(object):
    def __init__(self, tar, install_path, logger):
        self.tar = tar
        self.tarpath = os.path.dirname(tar)
        assert self.tarpath
        self.install_path = install_path
        self.package = os.path.basename(tar)
        self.name = self.package.split('.')[0]
        assert self.name
        self.unzip_path = os.path.join(self.tarpath, self.name)
        self.user = ''
        self.env_config = '/usr/local/easyops/deploy_init/env.config'
        self.logger = logger

    def read_package_conf_user(self):
        if self.name == 'easyops_init' or self.name == 'easyadmin':
            return 'root'

        pcyaml = os.path.join(self.unzip_path, 'main/deploy/package.conf.yaml')
        if not os.path.exists(pcyaml):
            return 'easyops'

        code, out = exec_cmd(['cat %s | grep ^user' % pcyaml], check_call=False, logger=self.logger)
        if code == 0 and 'root' in out:
            return 'root'

        return 'easyops'

    def can_update(self, curversion, toversion):
        # 对于不标准的版本号，默认放行
        if not semantic_version.validate(curversion) or not semantic_version.validate(toversion):
            return True

        if semantic_version.Version(curversion) >= semantic_version.Version(toversion):
            return False
        else:
            return True

    def app_started(self):
        if self.name == 'agent':
            return True

        install_path = self.install_path
        if self.name == 'install_agent':
            install_path = '/usr/local/easyops/agent'

        return is_app_started(install_path)

    def deploy(self):
        if not os.path.exists(self.tarpath):
            return True, u"ignore not exists package: %s" % self.tarpath

        if not self.unzip():
            return False, "unzip error"

        curversion = get_version('%s/version.ini' % self.install_path)
        toversion = get_version('%s/main/version.ini' % self.unzip_path)
        self.curversion = curversion
        self.toversion = toversion
        msg = 'finish %s success' % self.name
        ok = True
        if not curversion:
            self.logger.info('installing %s, version "%s"' % (self.name, toversion))
            ok, msg = self.install()
            if not ok:
                uclogger.error('install %s failed: %s', self.name, msg)
            else:
                uclogger.info('install %s: %s' % (self.name, toversion))
        elif self.can_update(curversion, toversion):
            self.logger.info('upgrading %s, from "%s" to "%s"' % (self.name, curversion, toversion))
            ok, msg = self.upgrade()
            if not ok:
                uclogger.error('upgrade %s: %s -> %s, error: %s', self.name, curversion, toversion, msg)
            else:
                uclogger.info('upgrade %s: %s -> %s' % (self.name, curversion, toversion))
        elif curversion == toversion:
            if self.name != "python_linux" and not self.app_started():
                msg = 'version updated, but not started'
            else:
                self.logger.info('%s has been upgraded' % self.name)
        else:
            # cannot upgrade
            ok = False
            msg = 'cannot upgrade %s from "%s" to "%s"' % (self.name, curversion, toversion)

        return ok, msg

    def unzip(self):
        # already unzip
        after_unzip_path = os.path.join(self.tarpath, os.path.basename(self.tarpath))
        if os.path.exists(after_unzip_path):
            self.user = self.read_package_conf_user()
            return True

        exec_cmd(['cd %s &&' % self.tarpath, '/bin/rm -rf', './%s' % self.name], logger=self.logger)
        # Fixme: we only support GNU tar here
        exec_cmd(
            ['tar', '--pax-option="delete=SCHILY.fflags,delete=LIBARCHIVE.xattr.security.selinux"', '-xzf', self.tar,
             '-C', self.tarpath], logger=self.logger)
        exec_cmd(['chmod 755 %s' % self.unzip_path], logger=self.logger)
        self.user = self.read_package_conf_user()
        return True

    def move(self, is_update=True):
        if is_update and self.name == 'python_linux':
            exec_cmd(['/bin/mv %s %s-%s' % (self.install_path, self.install_path, int(time.time()))], logger=self.logger)
            exec_cmd(['mkdir -p %s' % self.install_path], logger=self.logger)

        op_checker = opcache_checker.OpcacheChecker(self.logger, self.unzip_path, self.install_path)
        op_check_succ = False
        for i in range(3):
            exec_cmd(['/bin/cp -rf %s/main/* %s' % (self.unzip_path, self.install_path)], logger=self.logger)
            if op_checker.check():
                op_check_succ = True
                break
        if not op_check_succ:
            raise Exception("copy files error, unzip_path={0}, install_path={1}".format(self.unzip_path, self.install_path))

        exec_cmd(['chown -R easyops:easyops %s' % self.install_path], logger=self.logger)

    def backup(self):
        base_path, name = self.install_path.rsplit('/', 1)
        backup_name = '%s-%s.tar.gz' % (name, self.curversion)
        if os.path.exists(os.path.join(BACKUP_PATH, backup_name)):
            return 0, ''

        code, msg = exec_cmd(['cd %s' % base_path, '&&', 'tar czf %s %s' % (backup_name, name)],
                             check_call=False, logger=self.logger)

        # code == 0 or 1 means some files differ
        if code > 1:
            return code, msg
        code, msg = exec_cmd(['cd %s' % base_path, '&&', '/bin/mv -f %s %s' % (backup_name, BACKUP_PATH)],
                             check_call=False, logger=self.logger)

        return code, msg

    def is_cluster(self):
        code, out = exec_cmd(["/usr/local/easyops/deploy_init/tools/get_env.py common is_cluster"], logger=self.logger)
        if code != 0:
            raise Exception("get env error")
        return out == "true"

    def install(self):
        msg = u"install success"
        if not os.path.exists(self.install_path):
            os.makedirs(self.install_path)

        # su - easyops -c install_prescript.sh
        # install /usr/local/easyops/agent, in order to use easyops command, so donot need to prescript
        if self.install_path != '/usr/local/easyops/agent':
            pre_script = '%s/main/deploy/install_prescript.sh' % (self.unzip_path)
            self.exec_pre_script(pre_script)

        # move
        self.move(is_update=False)

        # python_linux特殊处理
        if self.name == 'python_linux':
            return True, msg

        if self.name == 'agent' and self.install_path == '/usr/local/easyops/agent':
            exec_cmd(['ln -snf /usr/local/easyops/agent/pkg/script/easyops.py /usr/bin/easyops'], logger=self.logger)
            return True, msg

        # su - easyops -c install_postscript.sh
        post_script = '%s/deploy/install_postscript.sh' % self.install_path
        self.exec_post_script(post_script)

        if self.name == 'agent':
            return True, msg

        # easyops init
        self.exec_easyops_cmd('init')

        # easyops start
        self.logger.info('starting %s' % self.name)
        code, msg = self.exec_easyops_cmd('start')

        # 特殊处理kafka，经常因为磁盘问题起不来
        if self.name == 'kafka' and code != 0:
            for i in range(10):
                code, msg = self.exec_easyops_cmd('restart')
                if code == 0:
                    break
        elif code != 0:
            for i in range(3):
                code, msg = self.exec_easyops_cmd('restart')
                if code == 0:
                    break

        if code:
            return False, msg

        return True, msg

    def after_install_start(self):
        """
        所有逻辑组件安装启动后，执行install_start_postscript.sh
        避免进程的割接互相依赖
        :return:
        """
        msg = "install start postscript success"
        # su - easyops -c install_start_postscript.sh
        post_script = '%s/deploy/install_start_postscript.sh' % self.install_path
        self.exec_post_script(post_script)
        return True, msg

    def after_update_start(self):
        """
        所有逻辑组件升级启动后，执行update_start_postscript.sh
        避免进程的割接互相依赖
        :return:
        """
        msg = "update start postscript success"
        if self.name in UPDATE_START_POSTSCRIPT_COMPONENTS:
            # su - easyops -c update_start_postscript.sh
            post_script = '%s/deploy/update_postscript.sh' % self.install_path
            # TODO: 之前用root执行的， 改回easyops执行， 要chown回去先, 后面要去掉这个chown
            if os.path.exists(post_script):
                cmd = ["chown -R easyops:easyops /data/easyops/{0}".format(self.name)]
                exec_cmd(cmd, check_call=False, logger=self.logger)
            self.exec_post_script(post_script)
        else:
            # su - easyops -c update_start_postscript.sh
            post_script = '%s/deploy/update_start_postscript.sh' % self.install_path
            # TODO: 之前用root执行的， 改回easyops执行， 要chown回去先, 后面要去掉这个chown
            if os.path.exists(post_script):
                cmd = ["chown -R easyops:easyops /data/easyops/{0}".format(self.name)]
                exec_cmd(cmd, check_call=False, logger=self.logger)
            self.exec_post_script(post_script)
        return True, msg

    def upgrade(self):
        msg = "upgade success"
        if self.install_path == '/usr/local/easyops/agent':
            return False, 'agent no need update'

        # backup
        if self.name != 'agent':
            code, _ = self.backup()
            if code:
                msg = 'backup %s failed' % self.name
                self.logger.error(msg)
                return False, msg

        # pre_stop
        # todo: 因为3.1.1前的nginx停止脚本比较暴力，会把uc的nginx也删掉，所以这里特殊处理nginx小于3.1.1的情况。
        #  当nginx版本号全部都大于等于3.1.1时，可以干掉这里对nginx的特殊处理
        if self.name == "nginx" and semantic_version.compare(self.curversion, "3.1.1") < 0:
            pass
        else:
            self.exec_easyops_cmd('stop')

        # su - easyops -c update_prescript.sh
        pre_script = '%s/main/deploy/update_prescript.sh' % (self.unzip_path)
        self.exec_pre_script(pre_script)

        if self.name == 'elasticsearch':
            curversion = map(int, self.curversion.split('.'))
            toversion = map(int, self.toversion.split('.'))
            if curversion < [2, 31, 0] and toversion >= [2, 31, 0]:
                ok = set_elasticsearch55_resource()
                if not ok:
                    msg = 'set elasticsearch55 resource failed'
                    self.logger.error(msg)
                    return False, msg

        # move
        self.move()

        # easyops init after package has been updated
        # so, if exec post script failed, just exec easyops restart after fix it
        # in case of forgetting to run easyops init
        self.exec_easyops_cmd('init')

        if self.name not in UPDATE_START_POSTSCRIPT_COMPONENTS:
            # su - easyops -c update_postscript.sh
            post_script = '%s/deploy/update_postscript.sh' % self.install_path
            self.exec_post_script(post_script)

        if self.install_path == '/data/easyops/agent':
            return True, msg

        # notify needs to re-init
        self.exec_easyops_cmd('init')

        # easyops restart
        self.logger.info('restarting %s' % self.name)
        code, msg = self.exec_easyops_cmd('restart')

        # 特殊处理kafka，经常因为磁盘问题起不来
        if self.name == 'kafka' and code != 0:
            for i in range(10):
                code, msg = self.exec_easyops_cmd('restart')
                if code == 0:
                    break
        elif code != 0:
            for i in range(3):
                code, msg = self.exec_easyops_cmd('restart')
                if code == 0:
                    break

        if code:
            return False, msg

        return True, msg

    def exec_easyops_cmd(self, param):
        if param not in ['start', 'stop', 'restart', 'init']:
            return 404, u'%s not support' % param

        if param == "start":
            param = "start --check_timeout=300"
        if param == "restart":
            param = "restart --check_timeout=300"
        cmd = ['cd %s' % self.install_path, '&&', 'easyops', param]
        count = 0
        while count < 3:
            if count:
                self.logger.info('retry to exec "%s"' % ' '.join(cmd))

            code, msg = exec_cmd(cmd, check_call=False, logger=self.logger)
            if code == 2005:  # get lock error
                count += 1
                time.sleep(1)
            else:
                break

        return code, msg

    def exec_pre_script(self, pre_script):
        if os.path.exists(pre_script):
            exec_cmd(['chmod', '+x', '%s' % self.tarpath], logger=self.logger)
            cmd = ['su - %s -c "cd %s && /bin/bash -x %s" -s /bin/bash' % (self.user, self.install_path, pre_script)]
            return exec_cmd(cmd, logger=self.logger)

    def exec_post_script(self, post_script):
        if os.path.exists(post_script):
            cmd = ['su - %s -c "cd %s && /bin/bash -x %s" -s /bin/bash' % (self.user, self.install_path, post_script)]
            return exec_cmd(cmd, logger=self.logger)
