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

# Copyright 2011-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
from os import path

from calculate.core.datavars import DataVars
from calculate.core.server.core_interfaces import MethodsInterface
from calculate.core.server.gen_pid import get_pid_info, ProcessMode
from calculate.lib.datavars import Variable
from calculate.lib.cl_log import log
from calculate.lib.utils.common import getPasswdUsers
from calculate.lib.utils.portage import isPkgInstalled, reVerSplitToPV
from calculate.lib.utils.colortext import get_color_print
import pwd
import glob
import calculate.lib.cl_template as cl_template

from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate

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

__ = getLazyLocalTranslate(_)


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


class UpdateLogger(MethodsInterface):
    """
    Логгер для обновления настроек системы
    """

    logger = log("apply-templates",
                 filename="/var/log/calculate/update_config.log",
                 formatter="%(asctime)s - %(levelname)s - %(message)s")

    def ERROR(self, *arg, **argv):
        """
        Вывести ошибку в лог и на экран
        """
        self.logger.error(arg[0])
        self.printERROR(*arg, **argv)

    def SUCCESS(self, *arg, **argv):
        """
        Вывести сообщение в лог и на экран
        """
        self.logger.info(arg[0])
        self.printSUCCESS(*arg, **argv)

    def WARNING(self, *arg, **argv):
        """
        Вывести предупреждение в лог и на экран
        """
        self.logger.warn(arg[0])
        self.printWARNING(*arg, **argv)


class ChainProgressTemplate(cl_template.ProgressTemplate):
    """
    Наложение шаблонов с определением перенастройки зависимых пакетов
    """

    def __init__(self, startTask, endTask, *args, **kwargs):
        self.startTask = startTask
        self.endTask = endTask
        cl_template.ProgressTemplate.__init__(self, *args, **kwargs)

    def changeMergePackage(self, packages):
        """
        Изменился настраиваемый пакет (по зависимостям)
        """
        self.endTask()
        packages = filter(isPkgInstalled,
                          packages)
        self.startTask(_("Configuring dependencies: %s") %
                       ",".join(packages))
        return True


class StubVariable(Variable):
    """
    Переменная-заглушка используется при обновлении настроек пакета
    в emerge. Если переменная не найдена, то будет возвращена пустая
    строка.
    """
    value = ""


class UpdateConfigs(UpdateLogger):
    """
    Обновить настройки пакета в пользовательских профилях
    """

    def init(self):
        self.color_print = get_color_print()

    def getXUsers(self):
        """
        Получить пользователей в X сессии
        """
        return list(
            self.clVars.Get('desktop.cl_desktop_online_user')) + ["root"]

    def getConfiguredPasswdUsers(self):
        """
        Получить пользоватлей, которые есть в /etc/passwd (UID>=1000)
        и при этом у них есть настройка профиля (.calculate/ini.env)
        """
        user, dn = 0, 1
        iniEnv = ".calculate/ini.env"
        return map(lambda x: x[user],
                   filter(lambda x: path.exists(path.join(x[dn], iniEnv)),
                          map(lambda x: (x, pwd.getpwnam(x).pw_dir),
                              getPasswdUsers())))

    def _setClMergePkg(self, clVars, category, nameProgram, slot=None):
        """
        Установить переменную cl_merge_pkg в зависимости от category и
        nameProgram
        """
        # выбрана перенастройка всех пакетов, установленных в системе
        if nameProgram == "all":
            clVars.Set("cl_merge_pkg",
                       map(lambda x: "{CATEGORY}/{PN}".format(**x),
                           filter(None,
                                  map(reVerSplitToPV,
                                      glob.glob('/var/db/pkg/*/*')))),
                       True)
        else:
            if slot:
                clVars.Set("cl_merge_pkg", ["%s/%s:%s" % (category,
                    nameProgram, slot)], True)
            else:
                clVars.Set("cl_merge_pkg", ["%s/%s" % (category, nameProgram)],
                           True)
        clVars.Set("cl_merge_set", "on", True)

    def updateDesktopConfig(self, nameProgram, version, slot, category,
                            configPath,
                            rootSet, verbose, dispatchConf, templates_locate,
                            ebuildPhase, useClt, arch_machine):
        """
        Настроить пакеты в профилях пользователей
        """
        # настраиватся будут пользователи из активных X сессии
        # и сконфигурированные
        xUsers = filter(lambda x: not "(unknown)" in x,
                        self.getXUsers())
        if not xUsers:
            self.logger.info(_("Package %s") % nameProgram)
            self.logger.warn(_("X session users not found"))
            return True
        self.logger.info(_("Package %s") % nameProgram)
        self.logger.info(_("Updating user configuration files"))
        firstValue = True

        clVars = DataVars()
        try:
            clVars.importData()
            clVars.flIniFile()
            setupable_users = set(xUsers + self.getConfiguredPasswdUsers())
            for userName in list(setupable_users):
                clVars.Set("cl_root_path", '/', True)
                clVars.Set("ur_login", userName, True)
                clVars.Set("cl_action", "desktop", True)
                clVars.Set("cl_verbose_set", verbose, True)
                clVars.Set("cl_protect_use_set", "off", True)
                clVars.Set("cl_template_path_use", templates_locate, True)
                clVars.Set("install.os_install_arch_machine", arch_machine,
                           True)

                self._setClMergePkg(clVars, category, nameProgram)

                clTempl = ChainProgressTemplate(self.startTask,
                                                self.endTask,
                                                self.setProgress,
                                                clVars, cltObj=False,
                                                printSUCCESS=self.printSUCCESS,
                                                printERROR=self.printERROR,
                                                askConfirm=self.askConfirm,
                                                printWARNING=self.printWARNING,
                                                printWarning=False)
                clTempl.onFirstValue = lambda *args: \
                    self.startTask(
                        _("User configuring the {nameProgram} package by "
                          "Calculate Utilities").format(
                            nameProgram=nameProgram))
                clTempl.firstValue = firstValue
                clTempl.applyTemplates()
                firstValue = clTempl.firstValue
            nofastlogin_users = set(getPasswdUsers()) - setupable_users
            fastlogin_path = self.clVars.Get(
                'desktop.cl_desktop_fastlogin_path')
            for user in nofastlogin_users:
                fastlogin_user = path.join(fastlogin_path, user)
                if path.exists(fastlogin_user):
                    try:
                        os.unlink(fastlogin_user)
                    except OSError:
                        pass
        finally:
            clVars.close()
            self.endTask()
        return True

    def updateSystemConfig(self, nameProgram, version, slot, category,
                           configPath,
                           rootSet, verbose, dispatchConf, templates_locate,
                           ebuildPhase, useClt, arch_machine):
        """
        Обновить конфигурационные файлы системы
        """
        self.logger.info(_("Package %s") % nameProgram)
        self.logger.info(_("Updating system cofiguration files"))
        if not os.path.exists(configPath):
            self.ERROR(_("Path '%s' does not exist") % configPath)
            return False

        clVars = DataVars()
        try:
            clVars.importData()
            clVars.flIniFile()
            clVars.Set("cl_root_path", configPath, True)

            # если конфигурирование пакета происходит не в корне
            if rootSet:
                # остальные пакеты настраиваются в корень
                clVars.Set("cl_root_path_next", '/', True)

            if self.clVars.Get('core.cl_core_pkg_slot_opt') != "all":
                self._setClMergePkg(clVars, category, nameProgram, slot)
            else:
                self._setClMergePkg(clVars, category, nameProgram)
            clVars.Set("cl_action", 'merge', True)
            clVars.Set("cl_verbose_set", verbose, True)
            clVars.Set("cl_dispatch_conf", dispatchConf, True)
            clVars.Set("cl_template_path_use", templates_locate, True)
            clVars.Set("core.cl_core_pkg_slot", slot, True)
            clVars.Set("install.os_install_arch_machine", arch_machine, True)
            useClt = useClt in (True, "on")

            dictVer = {slot: version}
            cl_template.templateFunction.installProg.update(
                {"%s/%s" % (category, nameProgram): dictVer,
                 "%s" % nameProgram: dictVer})

            # используем объект шаблонов
            # с clt шаблонами, clt фильтром, без использования postDispatchConf
            clTempl = ChainProgressTemplate(
                self.startTask,
                self.endTask,
                self.setProgress,
                clVars, cltObj=useClt,
                cltFilter=True,
                printSUCCESS=self.printSUCCESS,
                printERROR=self.printERROR,
                printWARNING=self.printWARNING,
                askConfirm=self.askConfirm,
                dispatchConf=self.dispatchConf
                if not ebuildPhase and self.isInteractive() else None,
                printWarning=False)
            # выводим сообщение о настройке пакета только если действительно
            # менялись файлы
            clTempl.onFirstValue = lambda *args: \
                self.startTask(_("System configuring for {nameProgram} "
                                 "package by Calculate Utilities").format(
                    nameProgram=nameProgram))
            clTempl.applyTemplates()
        finally:
            clVars.close()
            self.endTask()
        return True

    def patchPackage(self, configPath, nameProgram, arch_machine):
        """
        Наложить патчи на пакет
        """
        self.clVars.Set("cl_root_path", configPath, True)
        self.clVars.Set("install.os_install_arch_machine", arch_machine, True)
        clTempl = ChainProgressTemplate(self.startTask,
                                        self.endTask,
                                        self.setProgress,
                                        self.clVars, cltObj=False,
                                        printSUCCESS=self.printSUCCESS,
                                        printERROR=self.printERROR,
                                        askConfirm=self.askConfirm,
                                        printWARNING=self.printWARNING,
                                        printWarning=False)
        clTempl.onFirstValue = lambda *args: self.startTask(
            _("Using patches for the {nameProgram} package by "
              "Calculate Utilities").format(
                nameProgram=nameProgram),
            progress=True)
        clTempl.applyTemplates()
        return True

    def checkRunning(self):
        """
        Проверить наличие запущенных процессов в cl-core
        """
        from calculate.core.server.loaded_methods import LoadedMethods

        cur_pid = os.getpid()
        pid_list = [pid for pid in get_pid_info(self.clVars)
                    if (pid.get("mode", '') == ProcessMode.CoreDaemon and
                        pid.get("os_pid", '') != cur_pid)]
        if pid_list:
            _print = self.color_print
            method_names = {value[0]: value[2] for key, value in
                            LoadedMethods.conMethods.items()}
            self.printSUCCESS(
                _("Calculate core is executing the following tasks"))
            mult = _print.bold("*")
            for pid in pid_list:
                name = pid['name']
                method_name = method_names.get(name, name)
                self.printDefault(
                    "&nbsp;{mult} {title} ({name})".format(mult=mult,
                                                           title=method_name,
                                                           name=name))
            answer = self.askConfirm(
                _("Would you like to terminate these tasks?"), "no")
            if answer == "no":
                raise KeyboardInterrupt
        return True

    def restartService(self, service_name):
        """
        Перезапустить указанный сервис
        """
        import time

        time.sleep(1)
        os.system('/etc/init.d/%s restart &>/dev/null &' % service_name)
        return True

    def processConfig(self, nameProgram, version, slot, category,
                      verbose, dispatchConf, templates_locate,
                      ebuildPhase, useClt, arch_machine):
        """
        Обновить конфигурационные файлы системы
        """
        self.logger.info(_("Package %s") % nameProgram)
        self.logger.info(_("Updating system cofiguration files"))

        clVars = DataVars()
        try:
            clVars.importData()
            clVars.flIniFile()
            clVars.Set("cl_root_path", "/", True)

            if self.clVars.Get('core.cl_core_pkg_slot_opt') != "all":
                self._setClMergePkg(clVars, category, nameProgram, slot)
            else:
                self._setClMergePkg(clVars, category, nameProgram)
            clVars.Set("cl_action", 'config', True)
            clVars.Set("cl_verbose_set", verbose, True)
            clVars.Set("cl_dispatch_conf", dispatchConf, True)
            clVars.Set("cl_template_path_use", templates_locate, True)
            clVars.Set("core.cl_core_pkg_slot", slot, True)
            useClt = False

            #dictVer = {slot: version}
            #cl_template.templateFunction.installProg.update(
            #    {"%s/%s" % (category, nameProgram): dictVer,
            #     "%s" % nameProgram: dictVer})

            # используем объект шаблонов
            # с clt шаблонами, clt фильтром, без использования postDispatchConf
            clTempl = ChainProgressTemplate(
                self.startTask,
                self.endTask,
                self.setProgress,
                clVars, cltObj=useClt,
                cltFilter=True,
                printSUCCESS=self.printSUCCESS,
                printERROR=self.printERROR,
                printWARNING=self.printWARNING,
                askConfirm=self.askConfirm,
                dispatchConf=self.dispatchConf
                if not ebuildPhase and self.isInteractive() else None,
                printWarning=False)
            # выводим сообщение о настройке пакета только если действительно
            # менялись файлы
            clTempl.onFirstValue = lambda *args: \
                self.startTask(_("System configuring for {nameProgram} "
                                 "package by Calculate Utilities").format(
                    nameProgram=nameProgram))
            clTempl.applyTemplates()
        finally:
            clVars.close()
            self.endTask()
        return True
