# -*- coding: utf-8 -*-

# Copyright 2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.


import sys
import os
import stat
from os import path
from calculate.core.server.core_interfaces import MethodsInterface
from calculate.lib.utils.files import (makeDirectory, removeDir, tar_directory,
                                       FilePermission, find, listDirectory,
                                       FilesError, process,
                                       readFile, pathJoin, FindFileType)
from calculate.lib.utils.content import FileOwnersRestricted, ContentsStorage
from calculate.lib.utils.portage import getInstalledAtom
from calculate.lib.configparser import ConfigParserCaseSens
from calculate.lib.cl_template import Template

from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate
from variables.action import Actions
import tarfile
import shutil
import glob
from itertools import chain

_ = lambda x: x
setLocalTranslate('cl_core3', sys.modules[__name__])

__ = getLazyLocalTranslate(_)


class BackupError(Exception):
    """
    Исключение вызванное во время резервного копирования настроек
    """


class Backup(MethodsInterface):
    """
    Выполнение резервного копирования настроек
    """

    def init(self):
        self.apply_files = set()

    def prepare_backup(self, dn, rootname):
        makeDirectory(path.join(dn, rootname))
        return True

    def remove_directory(self, dn):
        removeDir(dn)
        return True

    def prepare_contents(self, dn, contents_file, rootname):
        dn_root = path.join(dn, rootname)
        fo = FileOwnersRestricted(
            "/", ["/%s" % x for x in find(path.join(dn, rootname),
                                          fullpath=False)])
        cs = ContentsStorage(contents_file, fo)
        cs.keep(dn_root, dn_root)
        return True

    def create_archive(self, dn, archfile):
        arch_dn = path.dirname(archfile)
        if not path.exists(arch_dn):
            makeDirectory(arch_dn)
            # TODO: !!!! восстановить смену прав
            # os.chmod(arch_dn, FilePermission.UserAll)
        tar_directory(dn, archfile)
        return True

    def open_archive(self, dn, archfile):
        makeDirectory(dn)
        with tarfile.open(archfile, 'r:bz2') as f:
            f.extractall(dn)
        return True

    def restore_configs(self, archfile, dn, contents_name, root_name):
        """
        Восстановить все файлы настроек
        :param archfile:
        :param dn:
        :return:
        """
        with tarfile.open(archfile, 'r:bz2') as f:
            try:
                # исключить из переноса файлы, которые принадлежат пакетам,
                # которые не установлены в системе
                contents = f.extractfile(f.getmember(contents_name))
                pkg_file = [x.split()[:3:2] for x in contents]
                not_installed_files = [
                    x for x in pkg_file
                    if not any(getInstalledAtom(x[0].partition(":")[0]))]
                skip_packages = sorted(list(
                    set([x[0] for x in not_installed_files])))
                if skip_packages:
                    self.printWARNING(
                        _("Ignored settings the following packages: %s") %
                        ", ".join(skip_packages))
                not_installed_files = [x[0] for x in not_installed_files]
            except KeyError:
                raise BackupError(_("CONTENTS file is not found"))
            for ti in (x for x in f if x.name.startswith("%s/" % root_name)):
                if ti.name[4:] in not_installed_files:
                    continue
                if ti.issym() and not path.exists(ti.linkpath):
                    continue
                if ti.name[5:]:
                    fn_system = path.join(dn, ti.path[5:])
                    if path.exists(fn_system):
                        stat_system = os.lstat(fn_system)
                        if (stat.S_IMODE(stat_system.st_mode) == ti.mode and
                                    stat_system.st_uid == ti.uid and
                                    stat_system.st_gid == ti.gid):
                            data_system = readFile(fn_system)
                            extr_file = f.extractfile(ti)
                            if extr_file:
                                data_ti = extr_file.read()
                                if data_system == data_ti:
                                    continue
                    ti.name = ti.name[5:]
                    f.extract(ti, dn)
                    if ti.isfile():
                        self.apply_files.add(fn_system)
        return True

    def sava_ini(self, section, key, val):
        ini = ConfigParserCaseSens(strict=False)
        ini.read(self.clVars.Get('cl_backup_ini_env'), encoding="utf-8")
        if not ini.has_section(section):
            ini.add_section(section)
        ini[section][key] = str(val)
        with open(self.clVars.Get('cl_backup_ini_env'), 'w') as f:
            ini.write(f)
        return True

    def load_ini(self, section, key):
        ini = ConfigParserCaseSens(strict=False)
        ini.read(self.clVars.Get('cl_backup_ini_env'), encoding="utf-8")
        return ini.get(section, key, fallback="")

    def save_initd(self, dn, root_name):
        """
        Сохранить список init.d
        :param dn:
        :param root_name:
        :return:
        """
        self.sava_ini("install", "init",
                      ','.join(listDirectory('/etc/init.d', fullPath=False)))
        dn_root = path.join(dn, root_name)
        for dn in ('/etc/runlevels/sysinit',
                   '/etc/runlevels/default',
                   '/etc/runlevels/boot'):
            try:
                dn_backup = pathJoin(dn_root, dn)
                if not path.exists(dn_backup):
                    makeDirectory(dn_backup)
                for fn in listDirectory(dn, fullPath=True):
                    if path.islink(fn):
                        link = os.readlink(fn)
                        symname = pathJoin(dn_root, fn)
                        if not path.lexists(symname):
                            os.symlink(link, symname)
            except (OSError, IOError) as e:
                raise BackupError(_("Failed to save service with autorun") +
                                  (_(": %s") % (str(e))))
        return True

    def backup_marked(self, source_dn, target_dn, subdn, root_name):
        """
        Сохранить файлы из указанного каталога, отмеченного комментариями
        выполнения шаблонов
        :return:
        """
        source_dn = path.join(source_dn, subdn)
        len_source_dn = len(source_dn)
        target_dn = path.join(target_dn, root_name, subdn)
        try:
            for fn in find(source_dn, filetype=FindFileType.RegularFile):
                if " Modified Calculate" in readFile(fn, headbyte=300):
                    target_fn = "%s%s" % (target_dn, fn[len_source_dn:])
                    if not path.exists(path.dirname(target_fn)):
                        makeDirectory(path.dirname(target_fn))
                    shutil.copy2(fn, target_fn)
                    fn_stat = os.lstat(fn)
                    os.chown(target_fn, fn_stat.st_uid, fn_stat.st_gid)
        except (OSError, IOError) as e:
            raise BackupError(_("Failed to backup templates configured files") +
                              (_(": %s") % (str(e))))
        return True

    def clear_autorun(self):
        """
        Удалить все файлы из автозапуска, которые сслаются на файлы из списка
        init.d
        :return:
        """
        files = ["/etc/init.d/%s" % x.strip()
                 for x in self.load_ini("install", "init").split(',')]
        for dn in ('/etc/runlevels/sysinit',
                   '/etc/runlevels/default',
                   '/etc/runlevels/boot'):
            for fn in listDirectory(dn, fullPath=True):
                if path.islink(fn) and os.readlink(fn) in files:
                    os.unlink(fn)
        return True

    def restore_contents(self, contentsfile, dn):
        cs = ContentsStorage(contentsfile)
        cs.restore(dn)
        return True

    def set_service_action(self):
        self.clVars.Set('core.cl_backup_action', Actions.Service, force=True)
        return True

    nm_name = "NetworkManager"
    nm_config = "/etc/NetworkManager"
    nm_connections = path.join(nm_config, "system-connections")

    def _do_service(self, service, action):
        """
        Выполнить действие с сервисом (restart, start, stop, zap)
        :param service:
        :param action:
        :return:
        """
        actions = {
            'restart': _("Failed to restart"),
            'start': _("Failed to start"),
            'stop': _("Failed to stop"),
            'zap': _("Failed to zap"),
            'status': _("Failed to get status of")
        }
        try:
            p = process(service, action)
            if p.failed():
                data = p.readerr().strip()
                if ("has started, but is inactive" not in data and
                            "will start when" not in data):
                    for line in data.split('\n'):
                        self.printERROR(line)
                    raise BackupError(_("{action} {name} service").format(
                        action=actions.get(action, action),
                        name=path.basename(service)
                    ))
        except FilesError:
            self.printERROR(
                _("{action} {name} service").format(
                    action=actions.get(action, action),
                    name=path.basename(service)))
            raise
        return True

    def stop_net_services(self):
        """
        Остановить все сетевые службы (и NM и openrc)
        :return:
        """
        self._do_service("/etc/init.d/netmount", "zap")
        for fn in chain(["/etc/init.d/%s" % self.nm_name],
                        glob.glob('/etc/init.d/net.*')):
            if fn.endswith('.lo') or not path.exists(fn):
                continue
            self._do_service(fn, "stop")
        return True

    def unlink_openrc_net_services(self, files):
        """
        Удалить сетевые сервисы openrc сервисы openrc
        :return:
        """
        for fn in glob.glob('/etc/init.d/net.*'):
            if fn.endswith('.lo') or not path.exists(fn) or fn in files:
                continue
            try:
                os.unlink(fn)
                self.apply_files.add(fn)
            except OSError as e:
                self.printERROR(_("Failed to remove %s service"),
                                path.basename(fn))
                self.printERROR(str(e))
        return True

    def is_networkmanager_backup(self, backup_path):
        """
        Проверить сетевой менеджер в резервной копии
        :param backup_path:
        :return:
        """
        return path.lexists(path.join(
            backup_path, "root/etc/runlevels/default/%s" % self.nm_name))

    def is_networkmanager_system(self):
        """
        Проверить сетевой менеджер в текущей системе
        :return:
        """
        return path.lexists("/etc/runlevels/default/%s" % self.nm_name)

    def restore_openrc_net_initd(self, files):
        """
        Восстановить сервисы net.* и запустить их
        :return:
        """
        for fn in files:
            if not path.exists(fn):
                os.symlink("/etc/init.d/net.lo", fn)
                self.apply_files.add(fn)
                print "Create symlink", "/etc/init.d/net.lo", fn
            self._do_service(fn, "start")
            print "Start service", fn
        return True

    def restore_files(self, backup_path, files):
        """
        Восстановить указанные файлы из backup/root
        :param backup_path:
        :param files: список файлов (поддерживаются глобальные символы)
        :return:
        """
        len_source_prefix = len(path.join(backup_path, "root"))
        for source in chain(*[glob.glob(pathJoin(backup_path, "root", x))
                              for x in files]):
            dest = source[len_source_prefix:]
            if path.lexists(source):
                dn = path.dirname(dest)
                if not path.exists(dn):
                    makeDirectory(dn)
                if path.lexists(dest):
                    if self.is_equal_system_backup(dest, source):
                        continue
                #print "COPY", source, "->", dest

                shutil.copy2(source, dest)
                fn_stat = os.lstat(source)
                os.chown(dest, fn_stat.st_uid, fn_stat.st_gid)
                self.apply_files.add(dest)
        return True

    def clear_nm_connections(self, backup_path):
        """
        Удалить доступные соединения для NetworkManager
        :return:
        """
        base_dir = pathJoin(backup_path, "root")
        for fn in listDirectory(self.nm_connections, fullPath=True):
            try:
                if not path.exists(pathJoin(base_dir, fn)):
                    os.unlink(fn)
                    self.apply_files.add(fn)
            except OSError as e:
                raise BackupError(str(e))

    def is_equal_system_backup(self, system_fn, backup_fn):
        """
        Проверить одинаковый ли файл в резервной копии и системе
        :param system_fn:
        :param backup_fn:
        :return:
        """
        if path.islink(system_fn) != path.islink(backup_fn):
            return False
        if path.islink(system_fn):
            return os.readlink(system_fn) == os.readlink(backup_fn)
        if path.isfile(system_fn) != path.isfile(backup_fn):
            return False
        data_system = readFile(system_fn)
        data_backup = readFile(backup_fn)
        if data_system != data_backup:
            return False
        stat_system = os.lstat(system_fn)
        stat_backup = os.lstat(backup_fn)
        if (stat.S_IMODE(stat_system.st_mode) !=
                stat.S_IMODE(stat_backup.st_mode) or
                    stat_system.st_uid != stat_backup.st_uid or
                    stat_system.st_gid != stat_backup.st_gid):
            return False
        return True

    def check_backup_for_network(self, backup_path, files):
        """
        Проверить конфигурационные файлы настройки сети на соответствие текущим
        :param backup_path:
        :return:
        """
        backup_nm = self.is_networkmanager_backup(backup_path)
        system_nm = self.is_networkmanager_system()
        # проверить совпадает ли сетевой менеджер
        if backup_nm != system_nm:
            return False
        # если nm проверить совпадение system-connections
        if backup_nm:
            connection_files = set(path.basename(x) for x in chain(
                glob.glob("%s/*" % self.nm_connections),
                glob.glob("%s/*" % (pathJoin(backup_path, "root",
                                             self.nm_connections)))
            ))
            for fn in connection_files:
                system_fn = pathJoin(self.nm_connections, fn)
                backup_fn = pathJoin(backup_path, "root",
                                     self.nm_connections, fn)
                if not self.is_equal_system_backup(system_fn, backup_fn):
                    return False
        # если openrc проверить conf.d/net и соответствие net.*
        else:
            system_fn = "/etc/conf.d/net"
            backup_fn = pathJoin(backup_path, "root", system_fn)
            if not self.is_equal_system_backup(system_fn, backup_fn):
                return False
            system_net_set = (set(path.basename(x)
                                  for x in glob.glob('/etc/init.d/net.*')) -
                              {"net.lo"})
            backup_net_set = set(path.basename(x) for x in files)
            if system_net_set != backup_net_set:
                return False
        return True

    def restore_network(self, backup_path):
        """
        Восстановить сеть из backup
        :param backup_path:
        :return:
        """
        files = ["/etc/init.d/%s" % x.strip()
                 for x in self.load_ini("install", "init").split(',')
                 if x.startswith("net.") and x != "net.lo"]
        if self.check_backup_for_network(backup_path, files):
            self.endTask("skip")
            return True
        self.stop_net_services()
        self.unlink_openrc_net_services(files)
        self.clear_nm_connections(backup_path)
        self.restore_files(backup_path, ["/etc/conf.d/hostname",
                                         "/etc/resolv.conf",
                                         "/etc/hosts"])
        self._do_service("/etc/init.d/hostname", "restart")
        if self.is_networkmanager_backup(backup_path):
            self.unlink_openrc_net_services([])
            self.restore_files(backup_path, [
                "/etc/NetworkManager/system-connections/*",
                "/etc/NetworkManager/dispatcher.d/*",
            ])
            self._do_service("/etc/init.d/NetworkManager", "start")
        else:
            self.restore_files(backup_path, ["/etc/conf.d/net"])
            self.restore_openrc_net_initd(files)
        self._do_service("/etc/init.d/netmount", "start")
        return True

    def special_backup(self, backup_path):
        """
        Выполнить специализирование резервное копирование модулей
        :param backup_path:
        :return:
        """
        for backup_obj in self.iterate_modules():
            backup_obj.backup(backup_path)
        return True

    def special_restore(self, backup_path):
        """
        Выполнить специализирование восстановление из резервной копии
        :param backup_path:
        :return:
        """
        for backup_obj in self.iterate_modules():
            backup_obj.restore(backup_path)
        return True

    def iterate_modules(self):
        """
        Перебрать все модули backup
        :return:
        """
        site_packages = [path.join(x, "calculate")
                         for x in sys.path
                         if (x.endswith('site-packages') and
                             x.startswith('/usr/lib'))]
        ret_list = []
        for module, modDir in chain(
                *map(lambda x: map(lambda y: (path.basename(y), y),
                                   listDirectory(x, True, True)),
                     site_packages)):
            if path.exists(path.join(modDir, "backup_%s.py" % module)):
                if not "calculate-%s" % module in ret_list:
                    ret_list.append("calculate-%s" % module)
        cl_backup = ret_list
        for pack in cl_backup:
            if pack:
                module_name = '%s.backup_%s' % (pack.replace("-", "."),
                                                pack.rpartition("-")[2])
                import importlib

                try:
                    backup_module = importlib.import_module(module_name)
                    backup_obj = backup_module.Backup(self, self.clVars)
                    yield backup_obj
                except ImportError:
                    sys.stderr.write(_("Unable to import %s") % module_name)

    def display_changed_configs(self):
        """
        Отобразить список восстановленных файлов
        :return:
        """
        t = Template(self.clVars, printWARNING=self.printWARNING,
                     printERROR=self.printERROR, printSUCCESS=self.printSUCCESS)
        t.verboseOutput(self.apply_files)
        return True
