# -*- 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, sys, re, time
from calculate.lib.encrypt import encrypt
from os import path
from calculate.lib.utils.files import pathJoin, FilePermission

from calculate.lib.cl_lang import setLocalTranslate, _
from functools import reduce

setLocalTranslate('cl_install3', sys.modules[__name__])


class MigrationError(Exception):
    pass


class _shareData(object):
    """Share class"""
    _reNumb = re.compile("^\d+$")

    def getDataInFile(self, fileName='', lenData=7):
        """Get data list from file"""
        with open(fileName) as f:
            return map(lambda x: x[:lenData],
                       filter(lambda x: len(x) >= lenData,
                              map(lambda x: x.rstrip().split(":"), f)))


class migrateGroups(_shareData):
    """Migrate group to new system"""

    maxGid = 65000
    minGid = 1000
    fileGroups = "/etc/group"

    def __init__(self, prefixNewSystem):
        self.prefixNewSystem = prefixNewSystem

    def getData(self, fileName=''):
        if not fileName:
            fileName = self.fileGroups
        return self.getDataInFile(fileName=fileName, lenData=4)

    def getThisData(self):
        """Get data migrate groups in this system"""
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and self.minGid <= int(
                              x[2]) <= self.maxGid,
                      self.getData())

    def getNewData(self):
        """Get data migrate groups in new system"""
        fileName = pathJoin(self.prefixNewSystem, self.fileGroups)
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and self.minGid <= int(
                              x[2]) <= self.maxGid,
                      self.getData(fileName=fileName))

    def getNewDataSystemGroups(self):
        """Get data system groups in new system"""
        fileName = pathJoin(self.prefixNewSystem, self.fileGroups)
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and \
                          (int(x[2]) > self.maxGid or int(x[2]) < self.minGid),
                      self.getData(fileName=fileName))

    def getNewProcessedData(self):
        """Get processed data migrate groups in new system"""
        # data this Group no users
        dataThisGroupsNoUsers = map(lambda x: x[:3] + [""], self.getThisData())
        dataNewGroups = self.getNewData()
        namesNewGroups = map(lambda x: x[0], dataNewGroups)
        gidsNewGroups = map(lambda x: x[2], dataNewGroups)
        for data in dataThisGroupsNoUsers:
            nameGroup = data[0]
            gid = data[2]
            if nameGroup in namesNewGroups:
                dataNewGroups = filter(lambda x: x[0] != nameGroup,
                                       dataNewGroups)
                namesNewGroups = map(lambda x: x[0], dataNewGroups)
                gidsNewGroups = map(lambda x: x[2], dataNewGroups)
            if gid in gidsNewGroups:
                dataNewGroups = filter(lambda x: x[2] != gid, dataNewGroups)
                namesNewGroups = map(lambda x: x[0], dataNewGroups)
                gidsNewGroups = map(lambda x: x[2], dataNewGroups)
        systemGroupsNewData = self.getNewDataSystemGroups()
        return systemGroupsNewData, dataNewGroups, dataThisGroupsNoUsers


class migrateUsers(_shareData):
    """Migrate users to new system"""

    maxId = 65000
    minId = 1000
    filePasswd = "/etc/passwd"

    def __init__(self, prefixNewSystem):
        self.prefixNewSystem = prefixNewSystem

    def getData(self, fileName=''):
        if not fileName:
            fileName = self.filePasswd
        return self.getDataInFile(fileName=fileName, lenData=7)

    def getThisData(self):
        """Get data migrate users in this system"""
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and self.minId <= int(
                              x[2]) <= self.maxId,
                      self.getData())

    def getNewData(self):
        """Get data migrate users in new system"""
        fileName = pathJoin(self.prefixNewSystem, self.filePasswd)
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and self.minId <= int(
                              x[2]) <= self.maxId,
                      self.getData(fileName=fileName))

    def getNewDataSystemUsers(self):
        """Get data system users in new system"""
        fileName = pathJoin(self.prefixNewSystem, self.filePasswd)
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and \
                          (int(x[2] > self.maxId) or int(x[2]) < self.minId),
                      self.getData(fileName=fileName))

    def getThisDataSystemUsers(self):
        """Get data system users in this system"""
        fileName = self.filePasswd
        return filter(lambda x: \
                          self._reNumb.match(x[2]) and \
                          (int(x[2] > self.maxId) or int(x[2]) < self.minId),
                      self.getData(fileName=fileName))

    def getNewProcessedData(self, migrateUsers=()):
        """Get processed data migrate users in new system"""
        dataThisUsers = self.getThisData()
        if migrateUsers:
            dataThisUsers = filter(lambda x: x[0] in migrateUsers,
                                   dataThisUsers)
        dataNewUsers = self.getNewData()
        namesNewUsers = map(lambda x: x[0], dataNewUsers)
        uidsNewUsers = map(lambda x: x[2], dataNewUsers)
        for data in dataThisUsers:
            nameUser = data[0]
            uid = data[2]
            if nameUser in namesNewUsers:
                dataNewUsers = filter(lambda x: x[0] != nameUser, dataNewUsers)
                namesNewUsers = map(lambda x: x[0], dataNewUsers)
                uidsNewUsers = map(lambda x: x[2], dataNewUsers)
            if uid in uidsNewUsers:
                dataNewUsers = filter(lambda x: x[2] != uid, dataNewUsers)
                namesNewUsers = map(lambda x: x[0], dataNewUsers)
                uidsNewUsers = map(lambda x: x[2], dataNewUsers)
        systemUsersNewData = self.getNewDataSystemUsers()
        systemUsersNewNames = map(lambda x: x[0], systemUsersNewData)
        systemUsersNewUids = map(lambda x: x[2], systemUsersNewData)
        systemUsersThisData = []
        if migrateUsers:
            # this users < minId
            systemUsersThisData = filter(lambda x: int(x[2]) < self.minId and \
                                                   x[0] in migrateUsers,
                                         self.getThisDataSystemUsers())
            for data in systemUsersThisData:
                nameUser = data[0]
                uid = data[2]
                if nameUser in systemUsersNewNames:
                    systemUsersNewData = filter(lambda x: x[0] != nameUser,
                                                systemUsersNewData)
                    systemUsersNewNames = map(lambda x: x[0],
                                              systemUsersNewData)
                    systemUsersNewUids = map(lambda x: x[2],
                                             systemUsersNewData)
                if uid in systemUsersNewUids:
                    systemUsersNewData = filter(lambda x: x[2] != uid,
                                                systemUsersNewData)
                    systemUsersNewNames = map(lambda x: x[0],
                                              systemUsersNewData)
                    systemUsersNewUids = map(lambda x: x[2],
                                             systemUsersNewData)
        return (systemUsersThisData, systemUsersNewData,
                dataNewUsers, dataThisUsers)


class migrateShadow(_shareData):
    """Migrate users to new system"""

    fileShadow = "/etc/shadow"

    def __init__(self, sysThisMigrateUsers, sysNewMigrateUsers, newMigrateUsers,
                 thisMigrateUsers, prefixNewSystem):
        self.prefixNewSystem = prefixNewSystem
        self.sysThisMigrateUsers = sysThisMigrateUsers
        self.sysNewMigrateUsers = sysNewMigrateUsers
        self.newMigrateUsers = newMigrateUsers
        self.thisMigrateUsers = thisMigrateUsers
        self.newFileName = pathJoin(self.prefixNewSystem, self.fileShadow)

    def getData(self, fileName=''):
        if not fileName:
            fileName = self.fileShadow
        return self.getDataInFile(fileName=fileName, lenData=9)

    def getThisData(self):
        """Get data migrate users in this system"""
        return filter(lambda x: x[0] in self.thisMigrateUsers, self.getData())

    def getNewData(self):
        """Get data migrate users in new system"""
        return filter(lambda x: x[0] in self.newMigrateUsers,
                      self.getData(fileName=self.newFileName))

    def getNewDataSystemShadow(self):
        """Get data system users in new system"""
        return filter(lambda x: x[0] in self.sysNewMigrateUsers,
                      self.getData(fileName=self.newFileName))

    def getThisDataSystemShadow(self):
        """Get data system users in this system"""
        return filter(lambda x: x[0] in self.sysThisMigrateUsers,
                      self.getData())

    def getNewProcessedData(self):
        """Get processed data migrate shadow in new system"""
        dataThisShadow = self.getThisData()
        dataNewShadow = self.getNewData()
        namesNewShadow = map(lambda x: x[0], dataNewShadow)
        for data in dataThisShadow:
            nameUser = data[0]
            if nameUser in namesNewShadow:
                dataNewShadow = filter(lambda x: x[0] != nameUser,
                                       dataNewShadow)
                namesNewShadow = map(lambda x: x[0], dataNewShadow)
        systemShadowNewData = self.getNewDataSystemShadow()
        systemShadowThisData = self.getThisDataSystemShadow()
        systemShadowNewNames = map(lambda x: x[0], systemShadowNewData)
        for data in systemShadowThisData:
            nameUser = data[0]
            if nameUser in systemShadowNewNames:
                systemShadowNewData = filter(lambda x: x[0] != nameUser,
                                             systemShadowNewData)
                systemShadowNewNames = map(lambda x: x[0], systemShadowNewData)
        return (systemShadowThisData, systemShadowNewData, dataNewShadow,
                dataThisShadow)


class migrate(object):
    """Migrate users ang groups to new system"""
    templateShadow = "%(user)s:%(hash)s:%(days)s:0:%(maxDays)s:%(warnDays)s:::"
    templateUser = "%(user)s:x:%(id)s:%(gid)s::/home/%(user)s:/bin/bash"
    templateGroup = "%(group)s:x:%(gid)s:"
    dataUsers = []
    dataGroups = []
    dataShadow = []
    maxId = 65000
    minId = 1000
    maxGid = 65000
    minGid = 1000
    minSysId = 1000

    newUserGroups = ["audio", "cdrom", "cdrw", "games", "lp", "lpadmin",
                     "plugdev", "scanner" "usb", "users", "video", "wheel"]

    def __init__(self, prefixNewSystem):
        self.prefixNewSystem = prefixNewSystem
        self.objGroups = migrateGroups(self.prefixNewSystem)
        self.objUsers = migrateUsers(self.prefixNewSystem)

    def addThisUsersToGroups(self, users):
        """Add users to groups"""
        thisGroupsData = self.objGroups.getData()
        thisGroupsData = map(lambda x: (x[0], x[3].split(',')),
                             thisGroupsData)
        dataGroups = []
        for data in self.dataGroups:
            groupName = data[0]
            thisUsersInGroup = map(lambda x: x[1],
                                   filter(lambda x: x[0] == groupName,
                                          thisGroupsData))
            thisUsersInGroup = reduce(lambda x, y: x + y, thisUsersInGroup, [])
            addUsers = list(set(thisUsersInGroup) & set(users))
            if addUsers:
                newUsersInGroup = data[3].split(',')
                for user in addUsers:
                    if not user in newUsersInGroup:
                        newUsersInGroup.append(user)
                data[3] = ','.join(filter(lambda x: x, newUsersInGroup))
            dataGroups.append(data)
        self.dataGroups = dataGroups
        return self.dataGroups

    def getNextUid(self):
        """get next uid"""
        listUid = map(lambda x: int(x[2]),
                      filter(lambda x: \
                                 self.objUsers._reNumb.match(x[2]) and \
                                 self.minId <= int(x[2]) <= self.maxId,
                             self.dataUsers))
        if listUid:
            return max(listUid) + 1
        return self.minId

    def getNextGid(self):
        """get next gid"""
        listGid = map(lambda x: int(x[2]),
                      filter(lambda x: \
                                 self.objGroups._reNumb.match(x[2]) and \
                                 self.minGid <= int(x[2]) <= self.maxGid,
                             self.dataGroups))
        if listGid:
            return max(listGid) + 1
        return self.minGid

    def isSystemUser(self, userName):
        if filter(lambda x: x[0] == userName and int(x[2]) <= self.minSysId,
                  self.dataUsers):
            return True
        return False

    def addUserToGroups(self, userName, userGroups):
        """Add users to groups"""

        dataGroups = []
        for data in self.dataGroups:
            groupName = data[0]
            if groupName in userGroups:
                usersInGroup = data[3].split(',')
                if not userName in usersInGroup:
                    usersInGroup.append(userName)
                data[3] = ','.join(filter(lambda x: x, usersInGroup))
            dataGroups.append(data)
        self.dataGroups = dataGroups
        return self.dataGroups

    def addUserToDefaultGroups(self, userName):
        """Add users to default groups"""
        return self.addUserToGroups(userName, self.newUserGroups)

    def changePassword(self, userName, pwdHash, maxDays="99999", warnDays="7"):
        if not filter(lambda x: x[0] == userName, self.dataUsers):
            raise MigrationError(_("User %s not found") % userName)
        indexFoundUser = False
        for i, data in enumerate(self.dataShadow):
            if data[0] == userName:
                indexFoundUser = i
                break
        if callable(pwdHash):
            pwdHash = pwdHash(userName)
            if pwdHash is False:
                return False
        shadowDict = {"user": userName,
                      "hash": pwdHash,
                      "days": str(int(time.time() / 86400)),
                      "maxDays": maxDays,
                      "warnDays": warnDays}
        shadowLine = self.templateShadow % shadowDict
        shadowList = shadowLine.split(":")
        if indexFoundUser is False:
            self.dataShadow.append(shadowList)
        else:
            self.dataShadow[indexFoundUser] = shadowList
        return True

    def addUser(self, userName, userGroups, pwdHash):
        """Add user"""
        # find user
        if filter(lambda x: x[0] == userName, self.dataUsers):
            return "EXISTS"
        else:
            strUid = str(self.getNextUid())
            strGid = str(self.getNextGid())
            groupName = userName
            dataExistGroup = filter(lambda x: x[0] == groupName,
                                    self.dataGroups)
            if dataExistGroup:
                strGid = dataExistGroup[0][2]
            else:
                # add group
                groupDict = {"group": groupName, "gid": strGid}
                groupLine = self.templateGroup % groupDict
                groupList = groupLine.split(":")
                self.dataGroups.append(groupList)
            # add user
            userDict = {"user": userName, "id": strUid, "gid": strGid}
            userline = self.templateUser % userDict
            userList = userline.split(":")
            self.dataUsers.append(userList)
            # add shadow
            if not self.changePassword(userName, pwdHash):
                return False
            # add user to default groups
            self.addUserToGroups(userName, userGroups)
            return True

    def checkPermFiles(self):
        """Check permission files"""
        checkThisFiles = [migrateGroups.fileGroups, migrateUsers.filePasswd,
                          migrateShadow.fileShadow]
        checkNewFiles = map(lambda x: pathJoin(self.prefixNewSystem, x),
                            checkThisFiles)
        parentDir = lambda x: "".join(os.path.split(x)[:-1])
        notRead = lambda x: not os.access(x, os.R_OK)
        notWrite = lambda x: not os.access(x, os.W_OK)
        filesNotRead = filter(notRead, checkThisFiles)
        if filesNotRead:
            raise MigrationError(_("Failed to read files") + _(": ") +
                                 ", ".join(filesNotRead))
        filesNotWrite = filter(notWrite, checkNewFiles)
        if filesNotWrite:
            raise MigrationError(_("Failed to write to files") + _(": ") +
                                 ", ".join(filesNotWrite))
        # Check permissions backup files
        checkNewBackupFiles = map(
            lambda x: pathJoin(self.prefixNewSystem, x + "-"),
            checkThisFiles)
        notWriteBackup = lambda x: not os.access(x, os.W_OK) and \
                                   (os.path.exists(x) or
                                    not os.access(os.path.dirname(x), os.W_OK))
        filesNotWrite = filter(notWriteBackup, checkNewBackupFiles)
        if filesNotWrite:
            raise MigrationError(_("Failed to write to files") + _(": ") +
                                 ", ".join(filesNotWrite))
        return True

    def saveNewFiles(self):
        """Save /etc/passwd /etc/group /etc/shadow to new system"""
        listFilesThisSystem = [migrateGroups.fileGroups,
                               migrateUsers.filePasswd,
                               migrateShadow.fileShadow]
        listFiles = map(lambda x: (pathJoin(self.prefixNewSystem, x),
                                   pathJoin(self.prefixNewSystem, x + "-")),
                        listFilesThisSystem)
        listData = [self.dataGroups, self.dataUsers, self.dataShadow]
        allData = zip(listFiles, listData)
        for fileNames, data in allData:
            buff = "\n".join(map(lambda x: ":".join(x), data)) + "\n"
            for fileName in fileNames:
                FD = open(fileName, "w+")
                FD.write(buff)
                FD.close()

    def createUserGuest(self):
        if filter(lambda x: int(x[2]) >= self.minSysId, self.dataUsers):
            return True
        else:
            # add user guest
            pwd = "guest"
            encryptObj = encrypt()
            pwdHash = encryptObj.getHashPasswd(pwd, "shadow_ssha256")
            if pwdHash is False:
                return False
            if not self.addUser("guest", "guest", pwdHash):
                return False
            return True

    def createHomeDirs(self, addUsersList, existsMigrateUsers):
        """Create home directories for all migreate users"""

        def createHome(userdata):
            perms = FilePermission.UserAll
            if not userdata[5].startswith('/dev/'):
                homedir = pathJoin(self.prefixNewSystem, userdata[5])
                if not path.exists(homedir):
                    os.mkdir(homedir)
                    os.chown(homedir, int(userdata[2]), int(userdata[3]))
                    os.chmod(homedir, perms)
        users = list(
            set(map(lambda x: x[0],
                    addUsersList) + existsMigrateUsers) - {"root"})
        try:
            map(createHome, filter(lambda x: x[0] in users, self.dataUsers))
        except Exception as e:
            raise MigrationError(
                _("Failed to create the user's home directory"))

    def migrate(self, addUsersList=None, rootPwd="",
                pwdUsersList=None, existsMigrateUsers=None):
        """Migrate users ang groups to new system"""
        if addUsersList is None:
            addUsersList = []
        elif not any(addUsersList):
            addUsersList = []
        if pwdUsersList is None:
            pwdUsersList = []
        if existsMigrateUsers is None:
            existsMigrateUsers = []
        if not self.checkPermFiles():
            return False
        migrateUsers = (["root"] +
                        map(lambda x: x[0], addUsersList + pwdUsersList))
        for existMigrUser in existsMigrateUsers:
            if existMigrUser not in migrateUsers:
                migrateUsers.append(existMigrUser)
        # add root to migrate users
        dataUsers = self.objUsers.getNewProcessedData(migrateUsers)
        dataGroups = self.objGroups.getNewProcessedData()
        thisSystemUsers, newSystemUsers, newUsers, thisUsers = \
            map(lambda x: map(lambda y: y[0], x), dataUsers)
        objShadow = migrateShadow(thisSystemUsers, newSystemUsers, newUsers,
                                  thisUsers, self.prefixNewSystem)
        dataShadow = objShadow.getNewProcessedData()
        self.dataGroups = reduce(lambda x, y: x + y, dataGroups, [])
        self.dataUsers = reduce(lambda x, y: x + y, dataUsers, [])
        self.dataShadow = reduce(lambda x, y: x + y, dataShadow, [])
        self.addThisUsersToGroups(thisUsers)
        for userName, pwdHash, maxDays, warnDays in pwdUsersList:
            if not self.changePassword(userName, pwdHash,
                                       maxDays=maxDays,
                                       warnDays=warnDays):
                return False
        for userName, userGroups, pwdHash in [
            ["root", [], rootPwd]] + addUsersList:
            # if self.isSystemUser(userName):
            #    raise MigrationError(_("%s is a system user") %userName)
            ret = self.addUser(userName, userGroups, pwdHash)
            if not ret:
                return False
            elif ret == "EXISTS":
                if not self.changePassword(userName, pwdHash):
                    return False
        if not newUsers or not thisUsers:
            # add user guest
            if not self.createUserGuest():
                return False
        self.saveNewFiles()
        self.createHomeDirs(addUsersList, existsMigrateUsers)
        return True


class currentUsers(migrate):
    """Current users"""

    def __init__(self):
        super(currentUsers, self).__init__('/')

    def addUsers(self, *users_passwd):
        """Added users and groups to current system"""
        if not self.checkPermFiles():
            return False
        getDataInFile = _shareData().getDataInFile
        self.dataUsers = getDataInFile(fileName=migrateUsers.filePasswd,
                                       lenData=7)
        self.dataGroups = getDataInFile(fileName=migrateGroups.fileGroups,
                                        lenData=4)
        self.dataShadow = getDataInFile(fileName=migrateShadow.fileShadow,
                                        lenData=9)
        getHash = encrypt().getHashPasswd
        for userName, pwd in zip(users_passwd[0::2],
                                 users_passwd[1::2]):
            pwdHash = getHash(pwd, "shadow_ssha256")
            if not self.addUser(userName, userName, pwdHash):
                return False
        self.saveNewFiles()
        return True

    def hasUsers(self, *users):
        """Is users in system"""
        if not self.checkPermFiles():
            return False
        getDataInFile = _shareData().getDataInFile
        self.dataUsers = map(lambda x: x[0],
                             getDataInFile(fileName=migrateUsers.filePasswd,
                                           lenData=7))
        return set(self.dataUsers) >= set(users)
