""" DIRAC FileCatalog component representing a directory tree with
    a closure table

    General warning: when we return the number of affected row, if the values did not change
                     then they are not taken into account, so we might return "Dir does not exist"
                     while it does.... the timestamp update should prevent this to happen, however if
                     you do it several times within 1 second, then there will be no changed, and affected = 0

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import six
import os

from DIRAC import S_OK, S_ERROR
from DIRAC.Core.Utilities.List import intListToString, stringListToString
from DIRAC.DataManagementSystem.DB.FileCatalogComponents.DirectoryManager.DirectoryTreeBase import DirectoryTreeBase

__RCSID__ = "$Id$"


class DirectoryClosure(DirectoryTreeBase):
    """Class managing Directory Tree with a closure table
    http://technobytz.com/closure_table_store_hierarchical_data.html
    http://fungus.teststation.com/~jon/treehandling/TreeHandling.htm
    http://www.slideshare.net/billkarwin/sql-antipatterns-strike-back
    http://dirtsimple.org/2010/11/simplest-way-to-do-tree-based-queries.html
    """

    def __init__(self, database=None):
        DirectoryTreeBase.__init__(self, database)
        self.directoryTable = "FC_DirectoryList"
        self.closureTable = "FC_DirectoryClosure"

    def findDir(self, path, connection=False):
        """Find directory ID for the given path

        :param path: path of the directory

        :returns: S_OK(id) and res['Level'] as the depth
        """

        dpath = os.path.normpath(path)
        result = self.db.executeStoredProcedure("ps_find_dir", (dpath, "ret1", "ret2"), outputIds=[1, 2])
        if not result["OK"]:
            return result

        if not result["Value"]:
            return S_OK(0)

        res = S_OK(result["Value"][0])
        res["Level"] = result["Value"][1]
        return res

    def findDirs(self, paths, connection=False):
        """Find DirIDs for the given path list

        :param paths: list of path

        :returns: S_OK( { path : ID} )
        """

        dirDict = {}
        if not paths:
            return S_OK(dirDict)
        dpaths = stringListToString([os.path.normpath(path) for path in paths])
        result = self.db.executeStoredProcedureWithCursor("ps_find_dirs", (dpaths,))
        if not result["OK"]:
            return result
        for dirName, dirID in result["Value"]:
            dirDict[dirName] = dirID

        return S_OK(dirDict)

    def removeDir(self, path):
        """Remove directory

        Removing a non existing directory is successful. In that case, DirID is 0

        :param path: path of the dir

        :returns: S_OK() and res['DirID'] the id of the directory removed
        """

        # Find the directory ID
        result = self.findDir(path)
        if not result["OK"]:
            return result

        # If the directory does not exist, we exit successfully with DirID = 0
        if not result["Value"]:
            res = S_OK()
            res["DirID"] = 0
            return res

        dirId = result["Value"]
        result = self.db.executeStoredProcedure("ps_remove_dir", (dirId,), outputIds=[])
        if not result["OK"]:
            return result

        result["DirID"] = dirId
        return result

    def existsDir(self, path):
        """Check the existence of a directory at the specified path

        :param path: directory path

        :returns: S_OK( { 'Exists' : False } ) if the directory does not exist
                  S_OK( { 'Exists' : True, 'DirID' : directory id  } ) if the directory exists
        """

        result = self.findDir(path)
        if not result["OK"]:
            return result
        if not result["Value"]:
            return S_OK({"Exists": False})
        else:
            return S_OK({"Exists": True, "DirID": result["Value"]})

    def getDirectoryPath(self, dirID):
        """Get directory name by directory ID

        :param dirID: directory ID

        :returns: S_OK(dir name), or S_ERROR if it does not exist

        """

        result = self.db.executeStoredProcedure("ps_get_dirName_from_id", (dirID, "out"), outputIds=[1])
        if not result["OK"]:
            return result

        dirName = result["Value"][0]

        if not dirName:
            return S_ERROR("Directory with id %d not found" % int(dirID))

        return S_OK(dirName)

    def getDirectoryPaths(self, dirIDList):
        """Get directory names by directory ID list

        :param dirIDList: list of dirIds
        :returns: S_OK( { dirID : dirName} )
        """

        dirs = dirIDList
        if not isinstance(dirIDList, list):
            dirs = [dirIDList]

        dirDict = {}

        # Format the list
        dIds = intListToString(dirs)
        result = self.db.executeStoredProcedureWithCursor("ps_get_dirNames_from_ids", (dIds,))
        if not result["OK"]:
            return result

        for dirId, dirName in result["Value"]:
            dirDict[dirId] = dirName

        return S_OK(dirDict)

    def getPathIDs(self, path):
        """Get IDs of all the directories in the parent hierarchy for a directory
        specified by its path, including itself

        :param path: path of the directory

        :returns: S_OK( list of ids ), S_ERROR if not found
        """

        result = self.findDir(path)
        if not result["OK"]:
            return result
        if not result["Value"]:
            return S_ERROR("Directory %s not found" % path)

        dirID = result["Value"]

        return self.getPathIDsByID(dirID)

    def getPathIDsByID(self, dirID):
        """Get IDs of all the directories in the parent hierarchy for a directory
        specified by its ID, including itself

        :param dirID: id of the dictionary

        :returns: S_OK( list of ids )

        """

        result = self.db.executeStoredProcedureWithCursor("ps_get_parentIds_from_id", (dirID,))

        if not result["OK"]:
            return result

        return S_OK([dId[0] for dId in result["Value"]])

    def getChildren(self, path, connection=False):
        """Get child directory IDs for the given directory"""
        if isinstance(path, six.string_types):
            result = self.findDir(path, connection)
            if not result["OK"]:
                return result
            if not result["Value"]:
                return S_ERROR("Directory does not exist: %s" % path)
            dirID = result["Value"]
        else:
            dirID = path

        result = self.db.executeStoredProcedureWithCursor("ps_get_direct_children", (dirID,))
        if not result["OK"]:
            return result
        if not result["Value"]:
            return S_OK([])

        return S_OK([x[0] for x in result["Value"]])

    def getSubdirectoriesByID(self, dirID, requestString=False, includeParent=False):
        """Get all the subdirectories of the given directory at a given level

        :param dirID: id of the directory
        :param requestString: if true, returns an sql query to get the information
        :param includeParent: if true, the parent (dirID) will be included

        :returns: S_OK ( { dirID, depth } ) if requestString is False
                 S_OK(request) if requestString is True

        """

        if requestString:
            reqStr = "SELECT ChildID FROM FC_DirectoryClosure WHERE ParentID = %s" % dirID
            if not includeParent:
                reqStr += " AND Depth != 0"
            return S_OK(reqStr)

        result = self.db.executeStoredProcedureWithCursor("ps_get_sub_directories", (dirID, includeParent))
        if not result["OK"]:
            return result
        if not result["Value"]:
            return S_OK({})

        return S_OK(dict((x[0], x[1]) for x in result["Value"]))

    def getAllSubdirectoriesByID(self, dirIdList):
        """Get IDs of all the subdirectories of directories in a given list

        :param dirList: list of dir Ids
        :returns: S_OK([ unordered dir ids ])
        """

        dirs = dirIdList
        if not isinstance(dirIdList, list):
            dirs = [dirIdList]

        dIds = intListToString(dirs)
        result = self.db.executeStoredProcedureWithCursor("ps_get_multiple_sub_directories", (dIds,))

        if not result["OK"]:
            return result

        resultList = [dirId[0] for dirId in result["Value"]]
        return S_OK(resultList)

    def getSubdirectories(self, path):
        """Get subdirectories of the given directory

        :param path: path of the directory

        :returns: S_OK ( { dirID, depth } )
        """

        result = self.findDir(path)
        if not result["OK"]:
            return result
        if not result["Value"]:
            return S_OK({})

        dirID = result["Value"]
        result = self.getSubdirectoriesByID(dirID)
        return result

    def countSubdirectories(self, dirId, includeParent=True):
        """Count the number of subdirectories

        :param dirID: id of the directory
        :param includeParent: count itself

        :returns: S_OK(value)
        """

        result = self.db.executeStoredProcedure(
            "ps_count_sub_directories", (dirId, includeParent, "ret1"), outputIds=[2]
        )
        if not result["OK"]:
            return result

        res = S_OK(result["Value"][0])
        return res

    ########################################################################################################
    #
    #  We overwrite some methods from the base class because of the new DB constraints or perf reasons
    #
    #  Some methods could be inherited in the future if we have perf problems. For example
    #  * removeDirectory
    #  * changeDirectory[Group/Owner/Mode]
    #  * getDirectoryPermissions (when called by getPathPermissions, we could buffer the getUserAndGroupID call)
    #  * getFileIDsInDirectory (used only by DirectoryMetadata)
    #  * getFilesInDirectory (used only by DirectoryMetadata)
    #  * getFileLFNsInDirectory (used only by FileMetadata)
    #  * getFileLFNsInDirectoryByDirectory (used only by FileMetadata)
    #  * _getDirectoryContents (we could bring together some requests)
    #
    ########################################################################################################

    def makeDirectory(self, path, credDict, status=1):
        """Create a directory

        :param path: has to be an absolute path. The parent dir has to exist
        :param credDict: credential dict of the owner of the directory
        :param: status ????

        :returns: S_OK (dirID) with a flag res['NewDirectory'] to True or False
                S_ERROR if there is a problem, or if there is no parent
        """

        if path[0] != "/":
            return S_ERROR("Not an absolute path")

        # Strip off the trailing slash if necessary
        dpath = os.path.normpath(path)
        parentDir = os.path.dirname(dpath)

        # Try to see if the dir exists
        result = self.findDir(path)
        if not result["OK"]:
            return result

        # if it does, we return it's id, with a flag NewDirectory to false
        dirID = result["Value"]
        if dirID:
            result = S_OK(dirID)
            result["NewDirectory"] = False
            return result

        # If it is the root directory, we force the owner to 'root'/'root' (id 1 in the db)
        if path == "/":
            l_uid = 1
            l_gid = 1
        else:
            # get the uid/gid of the owner
            result = self.db.ugManager.getUserAndGroupID(credDict)
            if not result["OK"]:
                return result
            (l_uid, l_gid) = result["Value"]

        # Find the ID of the parent
        res = self.findDir(parentDir)
        if not res["OK"]:
            return res

        parentDirId = res["Value"]

        # We only insert if there is a parent or if it is the root '/'
        if parentDirId or path == "/":
            result = self.db.executeStoredProcedureWithCursor(
                packageName="ps_insert_dir", parameters=(parentDirId, dpath, l_uid, l_gid, self.db.umask, status)
            )

            if not result["OK"]:
                return result

            dirId = result["Value"][0][0]

            result = S_OK(dirId)
            result["NewDirectory"] = True
            return result
        else:
            return S_ERROR("Cannot create directory without parent")

    def isEmpty(self, path):
        """Find out if the given directory is empty

        Rem: the speed could be enhanced if we were joining the FC_Files and FC_Directory* in the query.
            For the time being, it can stay like this

        :param path: path of the directory

        :returns: S_OK(true) if there are no file nor directorie, S_OK(False) otherwise
        """

        result = self.findDir(path)
        if not result["OK"]:
            return result
        dirId = result["Value"]

        if not dirId:
            return S_ERROR("Directory does not exist %s" % path)

        # Check if there are subdirectories
        result = self.countSubdirectories(dirId, includeParent=False)
        if not result["OK"]:
            return result

        subDirCount = result["Value"]
        if subDirCount:
            return S_OK(False)

        # Check if there are files in it
        result = self.db.fileManager.countFilesInDir(dirId)
        if not result["OK"]:
            return result

        fileCount = result["Value"]

        if fileCount:
            return S_OK(False)

        # If no files or subdir, it's empty
        return S_OK(True)

    def getDirectoryParameters(self, pathOrDirId):
        """Get parameters of the given directory

        :param pathOrDirID: the path or the id of the directory

        :returns: S_OK(dict), where dict has the following keys:
                        "DirID", "UID", "Owner", "GID", "OwnerGroup", "Status", "Mode", "CreationDate", "ModificationDate"
        """
        # Which procedure to use
        psName = None
        # it is a path ...
        if isinstance(pathOrDirId, six.string_types):
            psName = "ps_get_all_directory_info"
        # it is the dirId
        elif isinstance(pathOrDirId, ((list,) + six.integer_types)):
            psName = "ps_get_all_directory_info_from_id"
        else:
            return S_ERROR("Unknown type of pathOrDirId %s" % type(pathOrDirId))

        result = self.db.executeStoredProcedureWithCursor(psName, (pathOrDirId,))
        if not result["OK"]:
            return result

        # All the fields returned
        fieldNames = [
            "DirID",
            "UID",
            "Owner",
            "GID",
            "OwnerGroup",
            "Status",
            "Mode",
            "CreationDate",
            "ModificationDate",
        ]

        if not result["Value"]:
            return S_ERROR("Directory does not exist %s" % pathOrDirId)

        row = result["Value"][0]

        # Create a dictionary from the fieldNames
        rowDict = dict(zip(fieldNames, row))

        return S_OK(rowDict)

    def _setDirectoryParameter(self, path, pname, pvalue, recursive=False):
        """Set a numerical directory parameter


        Rem: the parent class has a more generic method, which is called
             in case we are given an unknown parameter

        :param path: path of the directory
        :param pname: name of the parameter to set
        :param pvalue: value of the parameter (an id or a value)

        :returns: S_OK(nb of row changed). It should always be 1 !
                S_ERROR if the directory does not exist
        """

        # The PS associated with a given parameter
        psNames = {
            "UID": "ps_set_dir_uid",
            "GID": "ps_set_dir_gid",
            "Status": "ps_set_dir_status",
            "Mode": "ps_set_dir_mode",
        }

        psName = psNames.get(pname, None)

        # If we have a recursive procedure and it is wanted, call it
        if recursive and pname in ["UID", "GID", "Mode"] and psName:
            psName += "_recursive"

        # If there is an associated procedure, we go for it
        if psName:
            result = self.db.executeStoredProcedureWithCursor(psName, (path, pvalue))

            if not result["OK"]:
                return result

            errno, affected, errMsg = result["Value"][0]
            if errno:
                return S_ERROR(errMsg)

            if not affected:
                # Either there were no changes, or the directory does not exist
                exists = self.existsDir(path).get("Value", {}).get("Exists")
                if not exists:
                    return S_ERROR(errno.ENOENT, "Directory does not exist: %s" % path)
                affected = 1

            return S_OK(affected)

        # In case this is a 'new' parameter, we have a fallback solution, but we should add a specific ps for it
        else:
            return DirectoryTreeBase._setDirectoryParameter(self, path, pname, pvalue)

    def _setDirectoryGroup(self, path, gname, recursive=False):
        """Set the directory owner"""

        result = self.db.ugManager.findGroup(gname)
        if not result["OK"]:
            return result

        gid = result["Value"]

        return self._setDirectoryParameter(path, "GID", gid, recursive=recursive)

    def _setDirectoryOwner(self, path, owner, recursive=False):
        """Set the directory owner"""

        result = self.db.ugManager.findUser(owner)
        if not result["OK"]:
            return result

        uid = result["Value"]

        return self._setDirectoryParameter(path, "UID", uid, recursive=recursive)

    def _setDirectoryMode(self, path, mode, recursive=False):
        """set the directory mode

        :param mixed path: directory path as a string or int or list of ints or select statement
        :param int mode: new mode
        """
        return self._setDirectoryParameter(path, "Mode", mode, recursive=recursive)

    def __getLogicalSize(self, lfns, ps_name, recursiveSum=True, connection=None):
        successful = {}
        failed = {}
        for path in lfns:
            result = self.findDir(path)
            if not result["OK"] or not result["Value"]:
                failed[path] = "Directory not found"
                continue

            dirID = result["Value"]
            result = self.db.executeStoredProcedureWithCursor(ps_name, (dirID, recursiveSum))

            if not result["OK"]:
                failed[path] = result["Message"]

            elif result["Value"]:
                successful[path] = {
                    "LogicalSize": int(result["Value"][0][0]),
                    "LogicalFiles": int(result["Value"][0][1]),
                }

                result = self.countSubdirectories(dirID, includeParent=False)
                if result["OK"]:
                    successful[path]["LogicalDirectories"] = result["Value"]
                else:
                    successful[path]["LogicalDirectories"] = -1

            else:
                successful[path] = {"LogicalSize": 0, "LogicalFiles": 0, "LogicalDirectories": 0}

        return S_OK({"Successful": successful, "Failed": failed})

    def _getDirectoryLogicalSizeFromUsage(self, lfns, recursiveSum=True, connection=None):
        """Get the total "logical" size of the requested directories"""
        return self.__getLogicalSize(lfns, "ps_get_dir_logical_size", recursiveSum=recursiveSum, connection=connection)

    def _getDirectoryLogicalSize(self, lfns, recursiveSum=True, connection=None):
        """Get the total "logical" size of the requested directories"""
        return self.__getLogicalSize(
            lfns, "ps_calculate_dir_logical_size", recursiveSum=recursiveSum, connection=connection
        )

    def __getPhysicalSize(self, lfns, ps_name, recursiveSum=True, connection=None):
        """Get the total size of the requested directories"""

        successful = {}
        failed = {}
        for path in lfns:
            result = self.findDir(path)
            if not result["OK"]:
                failed[path] = "Directory not found"
                continue
            if not result["Value"]:
                failed[path] = "Directory not found"
                continue
            dirID = result["Value"]

            result = self.db.executeStoredProcedureWithCursor(ps_name, (dirID, recursiveSum))
            if not result["OK"]:
                failed[path] = result["Message"]
                continue

            if result["Value"]:
                seDict = {}
                totalSize = 0
                totalFiles = 0
                for seName, seSize, seFiles in result["Value"]:
                    seDict[seName] = {"Size": int(seSize), "Files": int(seFiles)}
                    totalSize += seSize
                    totalFiles += seFiles
                seDict["TotalSize"] = int(totalSize)
                seDict["TotalFiles"] = int(totalFiles)
                successful[path] = seDict

            else:
                successful[path] = {}

        return S_OK({"Successful": successful, "Failed": failed})

    def _getDirectoryPhysicalSizeFromUsage(self, lfns, recursiveSum=True, connection=None):
        """Get the total size of the requested directories"""
        return self.__getPhysicalSize(
            lfns, "ps_get_dir_physical_size", recursiveSum=recursiveSum, connection=connection
        )

    def _getDirectoryPhysicalSize(self, lfns, recursiveSum=True, connection=None):
        """Get the total size of the requested directories"""
        return self.__getPhysicalSize(
            lfns, "ps_calculate_dir_physical_size", recursiveSum=recursiveSum, connection=None
        )

    def _changeDirectoryParameter(self, paths, directoryFunction, _fileFunction, recursive=False):
        """Bulk setting of the directory parameter with recursion for all the subdirectories and files

        :param dict paths: dictionary < lfn : value >, where value is the value of parameter to be set
        :param function directoryFunction: function to change directory(ies) parameter
        :param function fileFunction: function to change file(s) parameter
        :param bool recursive: flag to apply the operation recursively
        """

        arguments = paths
        successful = {}
        failed = {}
        for path, attribute in arguments.items():
            result = directoryFunction(path, attribute, recursive=recursive)
            if not result["OK"]:
                failed[path] = result["Message"]
            else:
                successful[path] = True

        return S_OK({"Successful": successful, "Failed": failed})
