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

# Copyright 2010-2012 Calculate Ltd. 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.


__version__ = "3.1.0"
__app__ = "calculate-core"

import sys, os
from shutil import copy2
import importlib
from os import path
import time
import re

from soaplib.serializers.primitive import String, Boolean, Integer
from soaplib.serializers.clazz import Array, ClassSerializer
from soaplib.service import rpc
from calculate.core.server.api_types import ReturnedMessage
from calculate.core.server.api_types import ViewInfo, ViewParams
from calculate.core.server.decorators import Dec
core_method = Dec.core_method
from func import safetyWrapper

from calculate.core.datavars import DataVarsCore,DataVars
from calculate.lib.datavars import Variable
from calculate.lib.cl_log import log
from calculate.lib import datavars
from calculate.lib.utils.files import (runOsCommand,scanDirectory,
                                       pathJoin,readFile)
from calculate.lib.utils.common import getPasswdUsers,getTupleVersion
from calculate.lib.utils.portage import isPkgInstalled,reVerSplitToPV
from calculate.lib.utils.content import getCfgFiles,PkgContents
import pwd
import glob
from calculate.lib.utils.files import getModeFile
import calculate.lib.cl_template as cl_template

from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate
setLocalTranslate('cl_core3',sys.modules[__name__])

__ = getLazyLocalTranslate(_)

class CoreSetupInfo(ClassSerializer):
    """Parameters for method core setup"""
    cl_core_pkg_name = String
    cl_core_pkg_version = String
    cl_core_pkg_slot = String
    cl_core_pkg_category = String
    cl_core_pkg_path = String
    cl_core_pkg_system_set = Boolean
    cl_core_pkg_desktop_set = Boolean
    cl_core_pkg_root_set = Boolean

    Default = Array(String)
    CheckOnly = Boolean

class shareUpdateConfigs:
    """Common methods"""

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

    def getFlagUpd(self,variables):
        """flagUpdate and programs"""
        return variables.Get("cl_autoupdate_set") == "on"

    def ERROR(self, *arg, **argv):
        """Print Error and log error"""
        # Запись в log ошибки
        self.logger.error(arg[0])
        self.printERROR(*arg, **argv)

    def SUCCESS(self, *arg, **argv):
        """Print and log success"""
        # Запись в log информации
        self.logger.info(arg[0])
        self.printSUCCESS(*arg, **argv)

    def WARNING(self, *arg, **argv):
        """Print and log warning"""
        # Запись в log предупреждения
        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),progress=True)
        return True

class StubVariable(Variable):
    value = ""

class updateUserConfigs(shareUpdateConfigs):
    """
    Update user profile for package
    """

    def getXUsers(self):
        """
        Get user login from Xsession
        """
        xSession = 0
        foundTwoSession = False
        retCode, resWho = runOsCommand("who")
        xUsers = []
        if retCode==0:
            if resWho:
                listProcessing = lambda x: (x[0], x[1], x[4])\
                                 if len(x)==5 else []
                xUsers = list(set(filter(lambda x: x!="root",
                              map(lambda x: x[0],
                                  filter(lambda x: x and\
                                         (x[2].startswith("(:") or \
                                          x[1].startswith(":")),
                                         map(lambda x: listProcessing(\
                                             filter(lambda y: y, x.split(" "))),
                                             resWho))))))
        else:
            self.printERROR(_("Failed to execute 'who'"))
            return False

        return xUsers

    def getConfiguredPasswdUsers(self):
        """
        Get users from passwd and discard users which has not
        .calculate/ini.env.
        """
        USER,DIR = 0,1
        iniEnv = ".calculate/ini.env"
        return map(lambda x:x[USER], 
               filter(lambda x:path.exists(path.join(x[DIR],iniEnv)), 
               map(lambda x:(x,pwd.getpwnam(x).pw_dir),
               getPasswdUsers())))

    def updateDesktopConfig(self, variables, clVars):
        """Update user configs"""
        nameProgram = variables.Get('cl_core_pkg_name')
        version = variables.Get('cl_core_pkg_version')
        slot = variables.Get('cl_core_pkg_slot')
        category = variables.Get('cl_core_pkg_category')
        configPath = variables.Get('cl_core_pkg_path')
        flagUpdate = self.getFlagUpd(variables)

        # X session user + configured local users (if user profile was not
        # configured then there is no need setup package for this user
        xUsers = filter(lambda x:not "(unknown)" in x,
                 list((set(self.getXUsers()) | 
                       set(self.getConfiguredPasswdUsers()))))
        if not xUsers:
            self.logger.info(_("Package %s") %nameProgram)
            self.logger.warn(_("X sessions users not found"))
            return True
        self.logger.info(_("Package %s") %nameProgram)
        self.logger.info(_("Updating user configuration files"))
        dictPakkages = {}
        listIndex = []
        mergeProgram = "calculate-utilities"
        firstValue = True
        for userName in xUsers:
            clVars.Set("cl_root_path", '/', True)
            clVars.Set("ur_login", userName, True)
            clVars.Set("cl_action", "desktop", True)
            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:
                clVars.Set("cl_merge_pkg", ["%s/%s"%(category,nameProgram)], True)
            clTempl = ChainProgressTemplate( self.startTask,
                               self.endTask,
                               self.setProgress,
                               clVars, cltObj = False,
                               printWarning=False)
            clTempl.onFirstValue = lambda *args: \
                    self.startTask(
                        _("User configuring the {nameProgram} package by "
                          "Calculate Utilities").format(
                          nameProgram=nameProgram),
                          progress=True)
            clTempl.firstValue = firstValue
            dirsFiles = clTempl.applyTemplates()
            firstValue = clTempl.firstValue
            if dirsFiles is False:
                self.printERROR(\
                    _("Error using templates for user %s")\
                        %userName)
                for errMess in clTempl.getError().splitlines():
                    self.printERROR(errMess)
                return False
            if dirsFiles and dirsFiles[1]:
                nameAndVerPkg = clVars.Get("cl_name")+"-"+\
                                clVars.Get("cl_ver")
                if not nameAndVerPkg in dictPakkages:
                    listIndex.append(nameAndVerPkg)
                    dictPakkages[nameAndVerPkg] = []
                dictPakkages[nameAndVerPkg].append((userName,
                    sorted(list(set(dirsFiles[1])))))
        self.endTask()
        if dictPakkages:
            for calcPkg in listIndex:
                self.printSUCCESS(_("Calculate Utilities have changed files")\
                                  +":")
                for userName, configFiles in dictPakkages[calcPkg]:
                    self.printSUCCESS(" "*2 + _("User %s")%userName + ":")
                    for nameConfigFile in configFiles:
                        self.printSUCCESS(" "*5 + nameConfigFile)
        if not dictPakkages:
             self.logger.warn(_("Template not found"))
        return True

class updateSystemConfigs(shareUpdateConfigs):
    """Update system configs"""

    def isExistsProtectFiles(self, configPath):
        """Есть ли в защищенных директориях конфигурационные файлы"""
        if not "CONFIG_PROTECT" in os.environ:
            self.ERROR(_("Wrong environment variable CONFIG_PROTECT"))

            exit(1)
        protectPaths = ["/etc"] + filter(lambda x: x.strip(),
                                      os.environ["CONFIG_PROTECT"].split(" "))
        flagFoundProtect = False
        for pPath in protectPaths:
            fPath = os.path.join(configPath, pPath[1:])
            if os.path.exists(fPath) and os.listdir(fPath):
                flagFoundProtect = True
                break
        if not flagFoundProtect:
            return False
        return True

    def scanProtectDirs(self, configPath,protectPaths):
        configFiles = []
        scanObj = scanDirectory()
        scanObj.processingFile = lambda path,prefix:configFiles.append(path) or\
                                                    True
        configPath = os.path.realpath(configPath)
        for pPath in protectPaths:
            realPath = pathJoin(configPath, pPath)
            if os.path.exists(realPath):
                scanObj.scanningDirectory(realPath)
        configFiles = map(lambda x: x.partition(configPath)[2], configFiles)
        configFiles = map(lambda x: pathJoin('/',x), configFiles)
        return configFiles

    def createDir(self, configPath, dstDir):
        """Create need dirs"""
        if os.path.exists(dstDir):
            return True

        def splPath(path):
            listPath = []
            if path in ("","/"):
                return []
            base, p = os.path.split(path)
            listPath.append(p)
            while(not base in ("","/")):
                base, p = os.path.split(base)
                listPath.append(p)
            listPath.reverse()
            return listPath
        notFoundPaths = []
        path = "/"
        for p in splPath(dstDir):
            path = os.path.join(path,p)
            if not os.path.exists(path):
                notFoundPaths.append(path)
        for mkPath in notFoundPaths:
            srcPath = pathJoin(configPath, mkPath)
            dMode, dUid, dGid = getModeFile(srcPath)
            os.mkdir(mkPath, dMode)
            os.chown(mkPath, dUid, dGid)
        return True

    def copyConfigFiles(self, configPath,configProtect):
        """Копирование конфигурационных файлов"""
        configDstFiles = self.scanProtectDirs(configPath,configProtect)
        if configDstFiles:
            self.logger.warn(_("Replaced file:"))
        for dst in configDstFiles:
            src = pathJoin(configPath, dst)
            if src != dst:
                dstDir = os.path.dirname(dst)
                self.createDir(configPath, dstDir)
                copy2(src, dst)
                sMode, sUid, sGid = getModeFile(src)
                os.chown(dst, sUid, sGid)
                os.chmod(dst, sMode)
                self.logger.warn(" "*5 + dst)
        return True

    def copyDirOrFile(self, src, dst, configPath):
        if src != dst:
            if os.path.isfile(src):
                dstDir = os.path.dirname(dst)
                self.createDir(configPath, dstDir)
                copy2(src, dst)
                sMode, sUid, sGid = getModeFile(src)
                os.chown(dst, sUid, sGid)
                os.chmod(dst, sMode)
            elif os.path.isdir(src):
                self.createDir(configPath, dst)
                sMode, sUid, sGid = getModeFile(src)
                os.chown(dst, sUid, sGid)
                os.chmod(dst, sMode)

    def updateSystemConfig(self, variables,clVars):
        #nameProgram, category, version, configPath):
        """Update system configs"""
        # get programs and flag update
        nameProgram = variables.Get('cl_core_pkg_name')
        version = variables.Get('cl_core_pkg_version')
        slot = variables.Get('cl_core_pkg_slot')
        category = variables.Get('cl_core_pkg_category')
        configPath = variables.Get('cl_core_pkg_path')
        #print "DESKTOP",variables.Get('cl_core_pkg_desktop_set')
        #print "SYSTEM",variables.Get('cl_core_pkg_system_set')
        flagUpdate = self.getFlagUpd(variables)
        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
        dictPakkages = {}
        listIndex = []
        clTempl = False
        #for mergeProgram in filter(None,mergePrograms):
        mergeProgram = "calculate-utilities"
        #if variables.Get('cl_ebuild_phase') == "preinst":
        #    self.startTask(_("Package configuring the {nameProgram} package by "
        #                     "Calculate Utilities").format(
        #                        nameProgram=nameProgram),
        #                   progress=True)
        #else:
        #    self.startTask(_("System configuring for {nameProgram} package by "
        #                     "Calculate Utilities").format(
        #                        nameProgram=nameProgram),
        #                   progress=True)
        clVars.Set("cl_root_path", configPath, True)
        if variables.Get('cl_core_pkg_root_set') == 'on':
            clVars.Set("cl_root_path_next", '/', True)
        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:
            clVars.Set("cl_merge_pkg", ["%s/%s"%(category,nameProgram)], True)
        clVars.Set("cl_action", 'merge', True)
        configFiles = []
        nameProg = clVars.Get("cl_name")
        #if nameProg == "calculate-core":
        #    configFiles = self.scanProtectDirs(configPath,
        #                       clVars.Get('cl_config_protect'))
        #    #q = PkgContents("%s/%s-%s"%(self.clVars.Get('cl_core_pkg_category'),
        #    #                self.clVars.Get('cl_core_pkg_name'),
        #    #                self.clVars.Get('cl_core_pkg_version')))
        #    #configFiles = q.content.keys()
        #if configFiles:
        #    #cltObject = cl_template.templateClt(clVars)
        #    #cltObject.filterApplyTemplates = configFiles
        clTempl = ChainProgressTemplate( self.startTask,
                           self.endTask,
                           self.setProgress,
                           clVars, cltObj=True,
                           cltFilter = True,
                           printSUCCESS=self.printSUCCESS,
                           printERROR=self.printERROR,
                           printWARNING=self.printWARNING,
                           printWarning=False)
        clTempl.onFirstValue = lambda *args: \
            self.startTask(_("System configuring for {nameProgram} package by "
                             "Calculate Utilities").format(
                             nameProgram=nameProgram),
                             progress=True)
        dirsFiles = clTempl.applyTemplates()
        # fix CONTENTS pkg file
        cfgs = getCfgFiles()
        autoUpdateDict = {}
        for pkg in list(set(clTempl.changedFiles.getPkgs()|
                        set(["%s/%s"%(category,nameProgram)]))):
            category = isPkgInstalled(pkg)
            if category:
                pkgContents = PkgContents("{CATEGORY}/{PF}".format(
                           **category[0]))
                for filename,action in clTempl.changedFiles.getPkgFiles(pkg):
                    if filename in clTempl.protectedFiles:
                        pkgContents.removeObject(filename)
                        continue
                    if action in (cl_template.ChangedFiles.FILE_MODIFIED,
                                  cl_template.ChangedFiles.DIR_CREATED,
                                  cl_template.ChangedFiles.DIR_EXISTS):
                        pkgContents.addObject(filename)
                    elif action in (cl_template.ChangedFiles.FILE_REMOVED,
                                    cl_template.ChangedFiles.DIR_REMOVED):
                        pkgContents.removeObject(filename)
                files = set(pkgContents.content.keys())
                if self.clVars.Get('cl_autoupdate_set') == 'off':
                    files &= set(clTempl.autoUpdateFiles)
                for filename in list(files&set(cfgs.keys())):
                    # get ctime from orig filename
                    if os.path.exists(filename):
                        ctime = os.stat(filename).st_ctime
                    else:
                        ctime = 0
                    # if orig filename older that .cfg
                    cfgs[filename].sort(reverse=True)
                    if ctime < cfgs[filename][0][0]:
                        try:
                            open(filename,'w').write(
                                readFile(cfgs[filename][0][1]))
                        except Exception as e:
                            self.printERROR(str(e))
                            self.printWARNING(
                                _("Failed to copy {ffrom} to {fto}").format(
                                 ffrom=cfgs[filename][0][1],fto=filename))
                            continue
                        autoUpdateDict[cfgs[filename][0][1]] = filename
                    for mtime,fn in cfgs[filename]:
                        try:
                            os.unlink(fn)
                        except Exception as e:
                            self.printWARNING(_("Failed to remove %s")%fn)
                pkgContents.writeContents()
        nameAndVerPkg = nameProg + "-"+clVars.Get("cl_ver")
        self.endTask()
        if dirsFiles is False:
            self.ERROR(_("Template error in package %s")\
                            %nameAndVerPkg)
            self.ERROR(clTempl.getError())
            return False
        if dirsFiles and dirsFiles[1]:
            if not nameAndVerPkg in listIndex:
                listIndex.append(nameAndVerPkg)
            dictPakkages[nameAndVerPkg] =\
                sorted(list(set(dirsFiles[1])))
        if dictPakkages:
            reGrey = re.compile(r"\._cfg\d{4}_")
            for calcPkg in listIndex:
                self.SUCCESS(_("Calculate Utilities have changed files")+":")
                for nameF in dictPakkages[calcPkg]:
                    nameFile = nameF.partition(configPath)[2]
                    if nameFile:
                        if nameFile[:1] != "/":
                            nameFile = "/" + nameFile
                    else:
                        nameFile = nameF
                    nameFile = autoUpdateDict.get(nameFile,nameFile)
                    if reGrey.search(nameFile):
                        self.SUCCESS(" "*5 + "<font color=\"dark gray\">%s</font>"%nameFile)
                    else:
                        self.SUCCESS(" "*5 + nameFile)
        else:
            self.logger.warn(_("Template not found"))
        if flagUpdate:
            self.copyConfigFiles(configPath,variables.Get('cl_config_protect'))
        if clTempl and clTempl.getWarning():
            for warn in clTempl.getWarning().split("\n"):
                self.WARNING(warn)
        return True

class PackageUpdater(updateSystemConfigs,updateUserConfigs):
    def __init__(self):
        self.clVars = None
        self.clTempl = None

    def initVars(self,datavars=None):
        """Primary variables initialization"""
        if not datavars:
            self.clVars = DataVars()
            self.clVars.importData()
            self.clVars.flIniFile()
        else:
            self.clVars = datavars

    def closeClTemplate(self):
        if self.clTempl:
            if self.clTempl.cltObj:
                self.clTempl.cltObj.closeFiles()
            self.clTempl.closeFiles()
            self.clTempl = None

    def postAction(self,error):
        """
        Post action for umount user res and write status_sync=error
        """
        self.closeClTemplate()
        return True

    @safetyWrapper(native_errors=(cl_template.TemplatesError,),
                   man_int=__("Manually interrupted"),
                   post_action=postAction)
    def updateConfig(self,dv):
        if not os.path.exists('/etc/calculate/calculate.env') and \
            not (dv.Get('cl_core_pkg_name') in ('calculate-utilities',
                                                'calculate-install') and \
                 getTupleVersion(dv.Get('cl_core_pkg_version')) >=
                 getTupleVersion("3.1.0_alpha4")):
            self.printWARNING(
                _("Configuration skipping until calculate-utilities are updated"))
            return True
        self.initVars(dv)
        clVars = DataVars()
        clVars.importData()
        clVars.flIniFile()
        slot = dv.Get('cl_core_pkg_slot')
        version = dv.Get('cl_core_pkg_version')
        category = dv.Get('cl_core_pkg_category')
        nameProgram = dv.Get('cl_core_pkg_name')
        # define that category/nameProgram installed
        if dv.Get('cl_ebuild_phase') in ('prerm','postrm'):
            version = ""
        dictVer = {slot:version}
        cl_template.templateFunction.installProg.update(\
                    {"%s/%s"%(category,nameProgram):dictVer,
                        "%s"%(nameProgram):dictVer})
        try:
            if os.environ.get("EBUILD_PHASE",""):
                clVars.raiseVariableNotFound = \
                    lambda name,*args,**kwargs: StubVariable(*args,
                                                             **kwargs)
                clVars.raiseModuleError = \
                    lambda name,*args,**kwargs: False
            if dv.Get('cl_core_pkg_system_set') == 'on':
                self.updateSystemConfig(dv,clVars)
            if dv.Get('cl_core_pkg_desktop_set') == 'on' and \
                not dv.Get('cl_ebuild_phase') in ("preinst","prerm"):
                self.updateDesktopConfig(dv,clVars)
        finally:
            if clVars:
                clVars.close()
        return True

class CoreWsdl:

    @rpc(Integer, CoreSetupInfo, _returns = Array(ReturnedMessage))
    @core_method(category=__('Configuration'),title=__("Configure a package"),
        image='preferences-desktop-default-applications',
        gui=True,command='cl-core-setup',
        rights=['configure'])
    def core_setup(self, sid, info):
        try:
            dv = self.get_cache(sid,"core_setup","vars")
            if not dv:
                dv = self.core_setup_vars()
            else:
                dv.processRefresh()
            if info:
                checkonly = info.CheckOnly
            else:
                checkonly = False
            errors = map(lambda x:ReturnedMessage(**x),
                     dv.checkGroups(info,allvars=not checkonly))
            if errors:
                return errors
            if checkonly:
                returnmess = ReturnedMessage(type = '', message = None)
                return [returnmess]
            install_meth = type("CommonCore",(self.Common,
                                PackageUpdater, object), {})
            pid = self.startprocess(sid, target=install_meth,
                            method="updateConfig",\
                            method_name="core_setup",
                            args_proc = (dv,))
            returnmess = ReturnedMessage(type = 'pid', message = pid)
            returnmess.type = "pid"
            returnmess.message = pid
            dv = self.clear_cache(sid,"core_setup")
            return [returnmess]
        finally:
            if dv:
                self.set_cache(sid,"core_setup","vars",dv,smart=False)
        return []

    def core_setup_vars(self,dv=None):
        if not dv:
            dv = DataVarsCore()
            dv.importCore()
            dv.flIniFile()
            dv.Set('cl_action','merge',True)
        dv.addGroup(None,
            normal=('cl_core_pkg_name',),
            expert=('cl_core_pkg_category', 'cl_core_pkg_version',
                    'cl_core_pkg_slot',
                    'cl_core_pkg_path', 'cl_core_pkg_system_set',
                    'cl_core_pkg_desktop_set', 'cl_core_pkg_root_set'),
            next_label=_("Setup"))
        return dv

    @rpc(Integer, ViewParams,_returns = ViewInfo)
    def core_setup_view (self, sid, params):
        dv = self.get_cache(sid,"core_setup","vars")
        if not dv:
            dv = self.core_setup_vars()
        else:
            dv.processRefresh()
        view = ViewInfo(dv,viewparams=params)
        self.set_cache(sid, 'core_setup', "vars",dv,smart=False)
        return view

