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

# Copyright 2010-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 os
from os import path
import sys
import time
from calculate.core.server.core_interfaces import MethodsInterface

from calculate.desktop._cl_keys import getKey

from calculate.lib.utils.mount import isMount, childMounts
from calculate.lib.utils.files import (process,
                                       getRunCommands, STDOUT,
                                       getLoopFromPath,
                                       getMdRaidDevices, listDirectory,
                                       removeDir, rsync_files, RsyncOptions,
                                       makeDirectory, getProgPath, FilesError)
from calculate.lib.utils.common import (mountEcryptfs,
                                        CommonError, isBootstrapDataOnly)

from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate

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

import tarfile
import tempfile
import shutil


class DesktopError(Exception):
    """Desktop Error"""


class Desktop(MethodsInterface):
    """
    Модуль для настройки пользовательского сеанса и выполнения
    принудительного выхода из X сессии пользователя
    """

    def __init__(self):
        self.homeDir = ""
        self.clTempl = None
        self.clVars = None

    def createCryptDir(self, userName, uid, gid, userDir,
                       recreateOnError=False):
        """
        Создать шифрование домашней директории, или подключить существующую

        userName,uid,gid,userDir: параметры пользовательской учётной записи
        recreateOnError: пересоздать профиль при ошбиках (используется при доменной
        ученой записи, так пользователь при этом ничего не теряет - профиль на сервере)
        """
        userPwd = getKey(userName)
        error = ""
        # проверить наличие пароля в ключах ядра
        if not userPwd or userPwd == "XXXXXXXX":
            raise DesktopError(_("User password not found"))
        ecryptfsPath = path.join('/home/.ecryptfs', userName)
        # если шифрование уже используется
        if path.exists(ecryptfsPath):
            for d in (".ecryptfs", ".Private"):
                source, target = path.join(ecryptfsPath, d), path.join(userDir,
                                                                       d)
                if not path.lexists(target):
                    os.symlink(source, target)
            # попытаться подключить шифрованные данные
            try:
                if not mountEcryptfs(userName, userPwd, userDir):
                    error = _("Failed to mount ecrypted data")
            except CommonError as e:
                error = (_("Failed to mount ecrypted data") +
                         _(": ") + '":%s"' % str(e))
        # если при подключении произошли ошибки
        if error:
            # заархивировать текущий профиль и удалить его
            if recreateOnError:
                self.printSUCCESS(_("Recovering encrypted data"))
                if self.getMountUserPaths(userDir):
                    raise DesktopError(_("Failed to encrypt the directory"))
                for source in (userDir, ecryptfsPath):
                    if path.exists(source):
                        if listDirectory(source):
                            target = source + ".bak"
                            newtarget = target
                            if path.exists(target):
                                removeDir(target)
                            os.rename(source, newtarget)
                        else:
                            os.rmdir(source)
                self.createUserDir(userName, uid, gid, userDir)
            # ошибка создания шифрования
            else:
                raise DesktopError(error)
        # если нет шифрованных данных
        if not path.exists(ecryptfsPath):
            tf = None
            remove_files = None
            try:
                # если профиль содержит только данные от бутстрапа core
                if isBootstrapDataOnly(userDir):
                    if childMounts(userDir):
                        raise DesktopError(
                            _("Failed to create an encrypted user profile") +
                            _(": ") +
                            _("The home directory contains mount points"))
                    # поместить данные во временный tarfile
                    remove_files = []
                    tf = tempfile.TemporaryFile()
                    with tarfile.open(fileobj=tf, mode='w:') as tarf:
                        for fn in listDirectory(userDir, fullPath=False):
                            full_fn = path.join(userDir, fn)
                            if path.lexists(full_fn):
                                tarf.add(full_fn, fn)
                                remove_files.append(full_fn)
                    tf.flush()
                    tf.seek(0)
                    # удалить эти данные
                    for fn in remove_files:
                        if not path.islink(fn) and path.isdir(fn):
                            shutil.rmtree(fn)
                        else:
                            os.unlink(fn)

                # создать шифрованные данные
                e = process('/usr/bin/ecryptfs-setup-private', '-u', userName,
                            '-b', '-l', userPwd, stderr=STDOUT)
                if e.failed():
                    raise DesktopError(e.read())
                # если были данные от бутстрапа, то распаковать их
                if tf and remove_files:
                    with tarfile.open(fileobj=tf, mode='r:') as tarf:
                        tarf.extractall(userDir)
            except Exception as e:
                # в случае ошибки сохраняем архив (с данными bootstrap)
                # из памяти в файловую систему
                if tf:
                    tf.seek(0)
                    bakArchName = path.join(userDir, ".calculate.tar.bz2")
                    with open(bakArchName, 'w') as f:
                        f.write(tf.read())
                raise DesktopError(
                    "%s\n%s" %
                    (str(e),
                     _("Failed to create an encrypted user profile")))
            finally:
                if tf:
                    tf.close()
        return True

    def createUserDir(self, userName, uid, gid, userDir, mode=0700):
        """
        Create user directory with need uid and gid
        """
        if not path.exists(userDir):
            os.makedirs(userDir)
            if mode:
                os.chmod(userDir, mode)
            os.chown(userDir, uid, gid)
            return True
        else:
            raise DesktopError(_("Path %s exists") % userDir)

    def umountUserRes(self, *umountPaths):
        """
        Отключить пользовательские ресурсы
        """
        if umountPaths and type(umountPaths[0]) == list:
            umountPaths = umountPaths[0]
        for umountPath in umountPaths:
            if not self.umountSleepPath(umountPath):
                return False
        return True

    def getMountUserPaths(self, homeDir=False):
        """
        Found user resources
        """
        if not homeDir:
            self.clVars.Get("ur_login")
            homeDir = self.clVars.Get("ur_home_path")
        if not homeDir:
            raise DesktopError(_("Failed to determine the home directory"))
        dirStart, dirEnd = path.split(homeDir)
        mountProfileDir = path.join(dirStart, ".%s" % dirEnd)
        mountRemoteProfileDir = path.join(dirStart, ".%s.remote" % dirEnd)
        with open("/proc/mounts") as f:
            return filter(lambda x: x.startswith(homeDir) or
                                    x.startswith(mountProfileDir) or
                                    x.startswith(mountRemoteProfileDir),
                          map(lambda x: x.split(" ")[1], f))

    def umountSleepPath(self, rpath):
        """
        Отмонтировать указанный путь, а также отключить используемые
        в этом пути loop устройства и raid
        """
        # check for mount
        umount_cmd = getProgPath('/bin/umount')
        fuser_cmd = getProgPath("/bin/fuser")
        loops = getLoopFromPath(rpath)
        if loops:
            setLoops = set(map(lambda x: x.partition('/dev/')[2], loops))
            mdInfo = getMdRaidDevices()
            for k, v in mdInfo.items():
                if setLoops & set(v):
                    self.umountSleepPath('/dev/%s' % k)
                    process('/sbin/mdadm', '--stop', '/dev/%s' % k).success()
            for loop in loops:
                self.umountSleepPath(loop)
                process('/sbin/losetup', '-d', loop).success()
        if isMount(rpath):
            for waittime in [0, 0.5, 1, 2]:
                time.sleep(waittime)
                process(umount_cmd, rpath).success()
                if not isMount(rpath):
                    return True
            process(fuser_cmd, "-km", rpath).success()
            for waittime in [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]:
                time.sleep(waittime)
                if not isMount(rpath):
                    return True
                process(umount_cmd, "-l", rpath).success()
            else:
                if isMount(rpath):
                    self.printERROR(_("Failed to unmount directory %s") % rpath)
                    return False
        return True

    def setFastlogin(self, urLogin):
        """
        Отметить пользователя, что для него может быть
        использовать "быстрой логин"
        """
        fastlogin = self.clVars.Get('cl_desktop_fastlogin_path')
        if not path.exists(fastlogin):
            makeDirectory(fastlogin)
        fastlogin_user = path.join(fastlogin, urLogin)
        if not path.exists(fastlogin_user):
            try:
                open(fastlogin_user, 'w').close()
                return True
            except IOError:
                self.printWARNING(_("Failed to create the fastlogin mark file"))
                return False

    def userLogout(self, urLogin):
        if self.clVars.GetBool('cl_desktop_elogind_set'):
            return self.userLogoutByElogind(urLogin)
        else:
            return self.userLogoutBySession(urLogin)

    def getElogindSessionId(self, urLogin):
        loginctl = getProgPath("/bin/loginctl")
        p = process(loginctl, "--no-legend")
        try:
            for line in p:
                cols = [x.strip() for x in line.split()]
                if len(cols) >= 3:
                    sessionid, uid, username = cols[:3]
                    if username == urLogin:
                        return sessionid
        finally:
            p.close()
        return None

    def terminateUserSession(self, session_id):
        loginctl = getProgPath("/bin/loginctl")
        p = process(loginctl, "--terminate-session", session_id)
        return p.success()

    def userLogoutByElogind(self, urLogin):
        elogin_session_id = self.getElogindSessionId(urLogin)
        if not elogin_session_id:
            raise DesktopError(_("Unable to detect user session id"))
        return self.terminateUserSession(elogin_session_id)

    def userLogoutBySession(self, urLogin):
        """
        Выполнить logout пользователя через dbus
        """
        display = self.clVars.Select('cl_desktop_online_display',
                                     where='cl_desktop_online_user', eq=urLogin,
                                     limit=1)
        session = self.clVars.Get('cl_desktop_xsession')
        if session == 'xfce':
            logoutCommand = "/usr/bin/qdbus org.xfce.SessionManager " \
                            "/org/xfce/SessionManager " \
                            "org.xfce.Session.Manager.Logout False False"
        elif session == 'kde':
            logoutCommand = "/usr/bin/kquitapp ksmserver"
        elif session == 'plasma':
            logoutCommand = "/usr/bin/qdbus org.kde.ksmserver /KSMServer " \
                            "org.kde.KSMServerInterface.logout 0 0 0"
        elif session in ('gnome', 'mate', 'cinnamon-session'):
            logoutCommand = "/usr/bin/qdbus org.gnome.SessionManager " \
                            "/org/gnome/SessionManager " \
                            "org.gnome.SessionManager.Logout 1"
        elif session == 'lxqt':
            logoutCommand = "/usr/bin/qdbus org.lxqt.session " \
                            "/LXQtSession org.lxqt.session.logout"
        else:
            raise DesktopError(_("Unable to detect the X session"))
        if process("su", urLogin, "-c",
                   ("DISPLAY=:%s " % display) + logoutCommand).failed():
            raise DesktopError(_("Unable to send the logout command"))
        return True

    def waitLogout(self, urLogin, waitTime, postWaitTime=20):
        """
        Ожидать завершения пользовательского сеанса

        Args:
        urLogin: логин пользователя
        waitTime: время ожидания завершения сеанса
        """
        session = self.clVars.Get('cl_desktop_xsession')
        if session == "plasma":
            uid = self.clVars.Get('ur_uid')
            if uid:
                uid = int(uid)
            for i in range(0, postWaitTime):
                if not filter(lambda x: "ksmserver" in x,
                              getRunCommands(uid=uid)):
                    break
                time.sleep(1)
        time.sleep(1)
        
        if filter(lambda x: "xdm/xdm\x00--logout" in x,
                  getRunCommands()):
            for i in range(0, waitTime):
                if not filter(lambda x: "xdm/xdm\x00--logout" in x,
                              getRunCommands()):
                    return True
                time.sleep(1)
            else:
                raise DesktopError(_("Unable to wait for completion "
                                     "of the user logout"))
        for wait in range(0, postWaitTime):
            self.clVars.Invalidate('cl_desktop_online_data')
            if urLogin not in self.clVars.Get('cl_desktop_online_user'):
                return True
            time.sleep(1)
        else:
            return False

    def prepareFace(self, ur_home_path):
        """Подготовить каталог пользователя с шифрованием для работы с .face
        
        Для шифрованных профилей в корне домашней директории
        создается симлинк на .ecryptfs/.face. В зашифрованном профиле
        такой симлинк будет создаваться шаблонами

        Args:
            ur_home_path: домашняя директория пользователя

        Returns:
            True/False в зависимости от успешности
        """
        if path.exists(ur_home_path):
            symlink_path = path.join(ur_home_path, '.face')
            if not path.lexists(symlink_path):
                os.symlink('.ecryptfs/.face', symlink_path)
            return True
        else:
            return False

    def syncSkel(self, ur_home_path, uid, gid):
        """
        Скопировать содержимое /etc/skel в каталог пользователя
        """
        try:
            rsync_files('/etc/skel', ur_home_path,
                        opts=(RsyncOptions.Archive,
                              RsyncOptions.Chown(uid, gid),))
        except FilesError as e:
            raise DesktopError(str(e))
        return True
