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

# Copyright 2012-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
import sys
import time
from calculate.lib.utils.files import getModeFile
from calculate.lib.cl_ldap import ldapUser

from calculate.lib.cl_lang import setLocalTranslate

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


class Printable(object):
    def __init__(self, parent):
        if isinstance(parent, Printable):
            self.parent = parent.parent
        self.parent = parent

    def printERROR(self, s):
        self.parent.printERROR(s)


class _shareData(Printable):
    """Share class"""
    fileName = ""
    template = ""
    lenData = 0
    data = []

    def getDataInFile(self, fileName='', lenData=0):
        """Get data list from file"""
        return filter(lambda x: len(x) == lenData,
                      map(lambda x: x.rstrip().split(":"), open(fileName)))

    def getFileAccess(self, perm="READ"):
        if perm == "READ":
            if os.access(self.fileName, os.R_OK):
                return True
            else:
                self.printERROR(_("Failed to read the file") + _(": ") +
                                self.fileName)
            return False
        elif perm == "WRITE":
            if os.access(self.fileName, os.W_OK):
                return True
            else:
                self.printERROR(_("Failed to write to file") + _(": ") +
                                self.fileName)
                return False

    def getData(self):
        if id(_shareData.data) == id(self.data):
            self.data = self.data[:]
        if self.data:
            return self.data
        elif self.getFileAccess(perm="READ"):
            self.data = self.getDataInFile(fileName=self.fileName,
                                           lenData=self.lenData)
            return self.data
        else:
            return False

    def save(self):
        if self.getFileAccess(perm="WRITE"):
            buff = "\n".join(map(lambda x: ":".join(x), self.data)) + "\n"
            FD = open(self.fileName, "w+")
            FD.write(buff)
            FD.close()
            return True
        else:
            return False

    def delete(self, name):
        if self.getData() is False:
            return False
        else:
            self.data = filter(lambda x: x[0] != name, self.data)
            return True

    def replace(self, name, listData):
        if self.getData() is False:
            return False
        else:
            flagFound = False
            for index, listDataOld in enumerate(self.data):
                if name == listDataOld[0]:
                    flagFound = True
                    if not self.equally(listData, self.data[index]):
                        self.data[index] = listData
            if flagFound:
                return True
            else:
                return None

    def equally(self, listDataA, listDataB):
        if set(listDataA) == set(listDataB):
            return True
        else:
            return False

    def get(self, name):
        if self.getData() is False:
            return False
        else:
            listData = filter(lambda x: x[0] == name, self.data)
            if listData:
                return listData[0]
            else:
                return []


class passwd(_shareData):
    '''Class for working with the file /etc/passwd'''
    fileName = "/etc/passwd"
    template = "%(login)s:x:%(uid)s:%(gid)s:%(gecos)s:%(directory)s:%(shell)s"
    lenData = 7

    def add(self, name, uid, gid, gecos="", directory="", shell="/bin/bash"):
        if not directory:
            directory = "/home/%s" % name
        userData = self.template % {'login': name, 'uid': uid, 'gid': gid,
                                    'gecos': gecos, 'directory': directory,
                                    'shell': shell}
        userList = userData.split(":")
        if self.getData() is False:
            return False
        else:
            ret = self.replace(name, userList)
            if ret is False:
                return False
            elif ret is None:
                self.data.append(userList)
            return True


class group(_shareData):
    '''Class for working with the file /etc/group'''
    fileName = "/etc/group"
    template = "%(group_name)s:x:%(gid)s:%(user_list)s"
    lenData = 4

    def add(self, name, gid, userList=()):
        groupData = self.template % {'group_name': name, 'gid': gid,
                                     'user_list': ','.join(userList)}
        groupList = groupData.split(":")
        if self.getData() is False:
            return False
        else:
            ret = self.replace(name, groupList)
            if ret is False:
                return False
            elif ret is None:
                self.data.append(groupList)
            return True

    def replace(self, name, listData):
        if self.getData() is False:
            return False
        else:
            delEmpty = lambda y: filter(lambda x: x.strip(), y)
            flagFound = False
            for index, listDataOld in enumerate(self.data):
                if name == listDataOld[0]:
                    flagFound = True
                    if not self.equally(listData, self.data[index]):
                        listDataWork = listData[:]
                        listDataWork[1] = self.data[index][1]
                        # Constant gid
                        listDataWork[2] = self.data[index][2]
                        # Join user list
                        userList = delEmpty(self.data[index][3].split(',') +
                                            listDataWork[3].split(','))
                        # unique list
                        userList = reduce(lambda x, y: \
                                              (y in x and x) or x + [y],
                                          userList, [])
                        listDataWork[3] = ','.join(userList)
                        self.data[index] = listDataWork
            if flagFound:
                return True
            else:
                return None

    def equally(self, listDataA, listDataB):
        getData = lambda x: x[:1] + x[3:]
        return _shareData.equally(self, getData(listDataA),
                                  getData(listDataB))

    def getSecondUserGroups(self, userName):
        """get all second user groups"""
        if self.getData() is False:
            return False
        else:
            userGroups = []
            for group, x, gid, userList in self.data:
                usersInGroup = userList.split(",")
                if userName in usersInGroup:
                    userGroups.append(group)
            return userGroups

    def getUsersInGroup(self, name):
        if self.getData() is False:
            return False
        else:
            dataGroup = map(lambda x: x[3].split(","),
                            filter(lambda x: x[0] == name, self.data))
            if dataGroup:
                return dataGroup[0]
            else:
                return []

    def deleteUserInGroups(self, userName, groups):
        if self.getData() is False:
            return False
        else:
            data = []
            for dataList in self.data:
                groupName, x, gid, userList = dataList
                if groupName in groups:
                    usersList = ",".join(filter(lambda x: x != userName,
                                                userList.split(",")))
                    dataList[3] = usersList
                data.append(dataList)
            self.data = data
            return self.data


class shadow(_shareData):
    '''Class for working with the file /etc/shadow'''
    fileName = "/etc/shadow"
    template = ("%(login)s:%(hash)s:%(shadowLastChange)s:"
                "%(shadowMin)s:%(shadowMax)s:%(shadowWarning)s:::")
    lenData = 9

    def add(self, name, pwdHash,
            shadowLastChange=str(int(time.time() / 86400)), shadowMin="0",
            shadowMax="99999", shadowWarning="7"):
        shadowData = self.template % {'login': name, 'hash': pwdHash,
                                      'shadowLastChange': shadowLastChange,
                                      'shadowMin': shadowMin,
                                      'shadowMax': shadowMax,
                                      'shadowWarning': shadowWarning}
        shadowList = shadowData.split(":")
        if self.getData() is False:
            return False
        else:
            ret = self.replace(name, shadowList)
            if ret is False:
                return False
            elif ret is None:
                self.data.append(shadowList)
            return True

    def equally(self, listDataA, listDataB):
        getData = lambda x: x[:1] + x[2:]
        return _shareData.equally(self, getData(listDataA),
                                  getData(listDataB))


class _shareCache(_shareData):
    def save(self):
        path = os.path.dirname(self.fileName)
        if not os.path.exists(path):
            try:
                os.makedirs(path)
            except OSError:
                self.printERROR(_("Failed to create directory %s") % path)
                return False
        if not os.path.exists(self.fileName):
            try:
                open(self.fileName, "w").close()
            except IOError:
                self.printERROR(_("Failed to create file %s") % self.fileName)
                return False
        if self.getFileAccess(perm="WRITE"):
            modeFile = 0600
            if getModeFile(self.fileName, mode="mode") != modeFile:
                os.chmod(self.fileName, modeFile)
            buff = "\n".join(map(lambda x: ":".join(x), self.data)) + "\n"
            FD = open(self.fileName, "w+")
            FD.write(buff)
            FD.close()
            return True
        else:
            return False

    def getData(self):
        if id(_shareData.data) == id(self.data):
            self.data = self.data[:]
        if self.data:
            return self.data
        elif not os.path.exists(self.fileName):
            return self.data
        elif self.getFileAccess(perm="READ"):
            self.data = self.getDataInFile(fileName=self.fileName,
                                           lenData=self.lenData)
            return self.data
        else:
            return False


class cachePasswd(_shareCache, passwd):
    fileName = "/var/lib/calculate/calculate-client/cache/passwd"


class cacheGroup(_shareCache, group):
    fileName = "/var/lib/calculate/calculate-client/cache/group"

    def replace(self, name, listData):
        return _shareData.replace(self, name, listData)

    def equally(self, listDataA, listDataB):
        return _shareData.equally(self, listDataA, listDataB)


class cacheShadow(_shareCache, shadow):
    fileName = "/var/lib/calculate/calculate-client/cache/shadow"


class cacheCreateGroup(cacheGroup):
    fileName = "/var/lib/calculate/calculate-client/cache/create_group"


class cacheCreatePasswd(cachePasswd):
    fileName = "/var/lib/calculate/calculate-client/cache/create_passwd"


class userCache(Printable):
    ldapObj = ldapUser()

    def addUserToCache(self, userName, pwdHash):
        '''Add LDAP user to cache'''
        ldapData = self.ldapObj.getUserLdapInfo(userName, shadowAttr=True)
        if not ldapData:
            self.printERROR(_("User %s not found in LDAP") % userName)
            return False
        groupName = ldapData['group']
        # Add user
        cachePasswdObj = cachePasswd(self)
        if not cachePasswdObj.add(userName, ldapData['uid'], ldapData['gid'],
                                  gecos=ldapData['fullName'],
                                  directory=ldapData['home'],
                                  shell=ldapData['loginShell']):
            return False
        if not cachePasswdObj.save():
            return False
        cacheGroupObj = cacheGroup(self)
        # Add primary group
        if not cacheGroupObj.add(groupName, ldapData['gid']):
            return False
        # Add second groups
        secondGroupsData = ldapData['groups']
        cacheSecondUserGroups = cacheGroupObj.getSecondUserGroups(userName)
        deleteSecondGroups = cacheSecondUserGroups
        if secondGroupsData:
            allUsersSecondGroups = []
            for groupName, gid in secondGroupsData:
                usersInGroup = cacheGroupObj.getUsersInGroup(groupName)
                if usersInGroup is False:
                    return False
                if not userName in usersInGroup:
                    usersInGroup.append(userName)
                if not cacheGroupObj.add(groupName, gid, usersInGroup):
                    return False
                allUsersSecondGroups.append(groupName)
            deleteSecondGroups = list(set(cacheSecondUserGroups) - \
                                      set(allUsersSecondGroups))
        if not cacheGroupObj.deleteUserInGroups(userName, deleteSecondGroups):
            return False
        if not cacheGroupObj.save():
            return False
        # Add shadow user
        cacheShadowObj = cacheShadow(self)
        if not cacheShadowObj.add(userName, pwdHash,
                                  shadowLastChange=ldapData['shadowLastChange'],
                                  shadowMin=ldapData['shadowMin'],
                                  shadowMax=ldapData['shadowMax'],
                                  shadowWarning=ldapData['shadowWarning']):
            return False
        if not cacheShadowObj.save():
            return False
        return True

    def delUserFromCacheCreate(self, userName):
        '''Delete LDAP user from createCache'''
        cacheCreatePasswdObj = cacheCreatePasswd(self)
        cacheUserData = cacheCreatePasswdObj.get(userName)
        if cacheUserData is False:
            return False
        if not cacheUserData:
            return True
        gid = cacheUserData[3]
        cacheCreateGroupObj = cacheCreateGroup(self)
        cacheSecondGroups = cacheCreateGroupObj.getSecondUserGroups(userName)
        if cacheSecondGroups is False:
            return False
        # delete user in second groups
        if not cacheCreateGroupObj.deleteUserInGroups(userName,
                                                      cacheSecondGroups):
            return False
        # delete user
        if not cacheCreatePasswdObj.delete(userName):
            return False
        if not cacheCreatePasswdObj.save():
            return False
        # delete groups
        usersGids = map(lambda x: x[3], cacheCreatePasswdObj.data)
        deleteGroups = map(lambda x: x[0],
                           filter(lambda x: not x[2] in usersGids and not x[3],
                                  cacheCreateGroupObj.data))
        for delGroupName in deleteGroups:
            if not cacheCreateGroupObj.delete(delGroupName):
                return False
        if not cacheCreateGroupObj.save():
            return False
        return True

    def delUserFromCache(self, userName):
        '''Delete LDAP user from cache'''
        cachePasswdObj = cachePasswd(self)
        cacheUserData = cachePasswdObj.get(userName)
        if cacheUserData is False:
            return False
        if not cacheUserData:
            return True
        gid = cacheUserData[3]
        cacheGroupObj = cacheGroup(self)
        cacheSecondGroups = cacheGroupObj.getSecondUserGroups(userName)
        if cacheSecondGroups is False:
            return False
        # delete user in second groups
        if not cacheGroupObj.deleteUserInGroups(userName, cacheSecondGroups):
            return False
        # delete user
        if not cachePasswdObj.delete(userName):
            return False
        if not cachePasswdObj.save():
            return False
        # delete groups
        usersGids = map(lambda x: x[3], cachePasswdObj.data)
        deleteGroups = map(lambda x: x[0],
                           filter(lambda x: not x[2] in usersGids and not x[3],
                                  cacheGroupObj.data))
        for delGroupName in deleteGroups:
            if not cacheGroupObj.delete(delGroupName):
                return False
        if not cacheGroupObj.save():
            return False
        # delete shadow user
        cacheShadowObj = cacheShadow(self)
        if not cacheShadowObj.delete(userName):
            return False
        if not cacheShadowObj.save():
            return False
        return True

    def delUserFromSystem(self, userName):
        '''Delete LDAP user from system files ( passwd, group, shadow )'''
        cacheCreatePasswdObj = cacheCreatePasswd(self)
        cacheCreatePasswdData = cacheCreatePasswdObj.get(userName)
        if cacheCreatePasswdData is False:
            return False
        if not cacheCreatePasswdData:
            return True
        passwdObj = passwd(self)
        userData = passwdObj.get(userName)
        if userData is False:
            return False
        # delete user
        if not userData:
            return True
        if not passwdObj.delete(userName):
            return False
        if not passwdObj.save():
            return False
        # delete user group
        groupObj = group(self)
        listGroupData = groupObj.getData()
        if listGroupData is False:
            return False
        cacheCreateGroupObj = cacheCreateGroup(self)
        secondUsersGroups = groupObj.getSecondUserGroups(userName)
        usersGids = map(lambda x: x[3], passwdObj.data)
        listGroupDataWork = []
        for index, groupData in enumerate(listGroupData):
            groupName, x, gid, listUsers = groupData
            listUsers = filter(lambda x: x.strip(), listUsers.split(','))
            listUsers = ",".join(filter(lambda x: x != userName, listUsers))
            cacheCreateGroupData = cacheCreateGroupObj.get(groupName)
            if cacheCreateGroupData is False:
                return False
            if groupName in cacheCreateGroupData:
                if not gid in usersGids and not listUsers:
                    continue
            listGroupDataWork.append([groupName, x, gid, listUsers])
        groupObj.data = listGroupDataWork
        if not groupObj.save():
            return False
        # delete user shadow
        shadowObj = shadow(self)
        shadowData = shadowObj.get(userName)
        if shadowData is False:
            return False
        if not shadowData:
            return True
        if not shadowObj.delete(userName):
            return False
        if not shadowObj.save():
            return False
        if not self.delUserFromCacheCreate(userName):
            return False
        return True

    def isConnectToLdap(self):
        connectData = self.ldapObj.getBindConnectData()
        if not connectData:
            return {}
        bindDn, bindPw, host = connectData
        usersDN = self.ldapObj.getUsersDN()
        groupsDNs = self.ldapObj.getGroupsDN()
        # Соединяемся с LDAP
        if not self.ldapObj.ldapConnect(bindDn, bindPw, host):
            return False
        return True

    def deleteCacheUsersFromSystem(self):
        '''Delete cache users from system'''
        cacheCreatePasswdObj = cacheCreatePasswd(self)
        cacheCreateListPasswdData = cacheCreatePasswdObj.getData()
        if cacheCreateListPasswdData is False:
            return False
        delUsersPasswd = map(lambda x: x[0], cacheCreateListPasswdData)
        for delUser in delUsersPasswd:
            if not self.delUserFromSystem(delUser):
                return False
        return True

    def getLoginDomainUsers(self):
        '''Get all domain login users'''
        cacheCreatePasswdObj = cacheCreatePasswd(self)
        cacheListCreatePasswdData = cacheCreatePasswdObj.getData()
        if cacheListCreatePasswdData is False:
            return False
        return cacheListCreatePasswdData

    def addCacheUsersFromSystem(self):
        '''Add cache users from system'''
        cachePasswdObj = cachePasswd(self)
        cacheListPasswdData = cachePasswdObj.getData()
        if not isinstance(cacheListPasswdData, (list, tuple)):
            return False
        # Add cache passwd users to system
        passwdObj = passwd(self)
        cacheCreatePasswdObj = cacheCreatePasswd(self)
        cacheListCreatePasswdData = cacheCreatePasswdObj.getData()
        if cacheListCreatePasswdData is False:
            return False
        # remove deleted users
        cacheUsers = map(lambda x: x[0], cacheListPasswdData)
        createUsers = map(lambda x: x[0], cacheListCreatePasswdData)
        deletedUsers = list(set(createUsers) - set(cacheUsers))
        for delUser in deletedUsers:
            if not self.delUserFromSystem(delUser):
                return False
            if not self.delUserFromCache(delUser):
                return False
        addUsers = []
        addUsersGid = []
        notAddUsers = []
        for cachePasswdData in cacheListPasswdData:
            userName, x, uid, gid, gecos, directory, shell = cachePasswdData
            retPasswd = passwdObj.get(userName)
            if retPasswd is False:
                return False
            if not retPasswd:
                if not cacheCreatePasswdObj.add(userName, uid, gid,
                                                gecos=gecos,
                                                directory=directory,
                                                shell=shell):
                    return False
            retCacheCreate = cacheCreatePasswdObj.get(userName)
            if retCacheCreate is False:
                return False
            if retCacheCreate:
                if not passwdObj.add(userName, uid, gid,
                                     gecos=gecos,
                                     directory=directory,
                                     shell=shell):
                    return False
                addUsers.append(userName)
                addUsersGid.append(gid)
            else:
                notAddUsers.append(userName)
        if passwdObj.data:
            if not passwdObj.save():
                return False
            if not cacheCreatePasswdObj.save():
                return False
        cacheShadowObj = cacheShadow(self)
        cacheListShadowData = cacheShadowObj.getData()
        if not isinstance(cacheListShadowData, (list, tuple)):
            return False
        # Add cache shadow users to system
        shadowObj = shadow(self)
        for cacheShadowData in cacheListShadowData:
            userName, pwdHash, shadowLastChange, shadowMin, shadowMax, \
            shadowWarning, x, x, x = cacheShadowData
            if userName in addUsers:
                if not shadowObj.add(userName, pwdHash,
                                     shadowLastChange=shadowLastChange,
                                     shadowMin=shadowMin,
                                     shadowMax=shadowMax,
                                     shadowWarning=shadowWarning):
                    return False
        if shadowObj.data:
            if not shadowObj.save():
                return False
        cacheGroupObj = cacheGroup(self)
        cacheListGroupData = cacheGroupObj.getData()
        if not isinstance(cacheListGroupData, (list, tuple)):
            return False
        cacheCreateGroupObj = cacheCreateGroup(self)
        # Add cache group users to system
        groupObj = group(self)
        setAddUsers = set(addUsers)
        for cacheGroupData in cacheListGroupData:
            groupName, x, gid, listUsers = cacheGroupData
            retGroup = groupObj.get(groupName)
            if retGroup is False:
                return False
            listUsers = filter(lambda x: x.strip(), listUsers.split(','))
            if setAddUsers & set(listUsers) or gid in addUsersGid:
                listUsers = filter(lambda x: not x in notAddUsers, listUsers)
                if not retGroup:
                    if not cacheCreateGroupObj.add(groupName, gid, listUsers):
                        return False
                if not groupObj.add(groupName, gid, listUsers):
                    return False
        if groupObj.data:
            if not groupObj.save():
                return False
            if cacheCreateGroupObj.data:
                if not cacheCreateGroupObj.save():
                    return False
        return True

    def syncCacheToLdap(self):
        '''Compare cache users to LDAP users.

        Deleting a user from the cache when the differences'''
        if not self.isConnectToLdap():
            self.printERROR(_("Failed to connect to the LDAP server"))
            return False
        cachePasswdObj = cachePasswd(self)
        cacheListPasswdData = cachePasswdObj.getData()
        if cacheListPasswdData is False:
            return False
        if (not isinstance(cacheListPasswdData, (tuple, list)) or
                not cacheListPasswdData):
            return True
        cacheGroupObj = cacheGroup(self)
        cacheListGroupData = cacheGroupObj.getData()
        if cacheListGroupData is False:
            return False
        cacheShadowObj = cacheShadow(self)
        deletedCacheUsers = []
        for cachePasswdData in cacheListPasswdData:
            userName, x, uid, gid, gecos, directory, shell = cachePasswdData
            ldapData = self.ldapObj.getUserLdapInfo(userName, shadowAttr=True)
            if not ldapData:
                deletedCacheUsers.append(userName)
                continue
            cacheGroupData = map(lambda x: x[0], filter(lambda x: x[2] == gid,
                                                        cacheListGroupData))
            if not cacheGroupData:
                deletedCacheUsers.append(userName)
                continue
            groupName = cacheGroupData[0]
            cacheShadowData = cacheShadowObj.get(userName)
            if cacheShadowData is False:
                return False
            if not cacheShadowData:
                deletedCacheUsers.append(userName)
                continue
            x, x, shadowLastChange, shadowMin, shadowMax, shadowWarning, \
            x, x, x = cacheShadowData
            groups = cacheGroupObj.getSecondUserGroups(userName)
            gidsGroups = map(lambda x: x[2],
                             filter(lambda x: x[0] in groups,
                                    cacheGroupObj.data))
            userShadowDict = {'uid': uid,
                              'gid': gid,
                              'fullName': gecos,
                              'home': directory,
                              'group': groupName,
                              'groups': (groups, gidsGroups),
                              'loginShell': shell,
                              'shadowLastChange': shadowLastChange,
                              'shadowMin': shadowMin,
                              'shadowMax': shadowMax,
                              'shadowWarning': shadowWarning}
            flagDeleteUser = False
            for attr, value in userShadowDict.items():
                if attr == "groups":
                    for index, val in enumerate(value):
                        if set(map(lambda x: x[index],
                                   ldapData[attr])) != set(val):
                            flagDeleteUser = True
                            break
                else:
                    if ldapData[attr] != value:
                        flagDeleteUser = True
                        break
            if flagDeleteUser:
                deletedCacheUsers.append(userName)
                continue
        # Deleted cache users
        for delUserName in deletedCacheUsers:
            if not self.delUserFromCache(delUserName):
                return False
        return True

    def clearCache(self):
        '''Clear cache files'''
        cacheObjs = (cachePasswd(self), cacheShadow(self), cacheGroup(self),
                     cacheCreateGroup(self), cacheCreatePasswd(self))
        for cacheObj in cacheObjs:
            if not cacheObj.save():
                return False
        return True
