""" DIRAC FileCatalog component representing a directory tree with enumerated paths
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# pylint: disable=protected-access


__RCSID__ = "$Id$"

import six
import os

from DIRAC import S_OK, S_ERROR
from DIRAC.DataManagementSystem.DB.FileCatalogComponents.DirectoryManager.DirectoryTreeBase import DirectoryTreeBase

MAX_LEVELS = 15


class DirectoryLevelTree(DirectoryTreeBase):
  """ Class managing Directory Tree as a simple self-linked structure
      with full directory path stored in each node
  """

  def __init__(self, database=None):
    DirectoryTreeBase.__init__(self, database)
    self.treeTable = 'FC_DirectoryLevelTree'

  def getTreeType(self):

    return 'Directory'

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

    dpath = self.db._escapeString(os.path.normpath(path))
    if not dpath['OK']:
      return dpath
    dpath = dpath['Value']
    req = "SELECT DirID,Level from FC_DirectoryLevelTree WHERE DirName=%s" % dpath
    result = self.db._query(req, connection)
    if not result['OK']:
      return result

    if not result['Value']:
      return S_OK('')

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

  def findDirs(self, paths, connection=False):
    """ Find DirIDs for the given path list
    """
    dpathList = []
    for path in paths:
      dpath = self.db._escapeString(os.path.normpath(path))
      if not dpath['OK']:
        return dpath
      dpathList.append(dpath['Value'])
    dpaths = ','.join(dpathList)
    req = "SELECT DirName,DirID from FC_DirectoryLevelTree WHERE DirName in (%s)" % dpaths
    result = self.db._query(req, connection)
    if not result['OK']:
      return result
    dirDict = {}
    for dirName, dirID in result['Value']:
      dirDict[dirName] = dirID

    return S_OK(dirDict)

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

    result = self.findDir(path)
    if not result['OK']:
      return result
    if not result['Value']:
      res = S_OK()
      res["DirID"] = 0
      return res

    dirID = result['Value']
    req = "DELETE FROM FC_DirectoryLevelTree WHERE DirID=%d" % dirID
    result = self.db._update(req)
    result['DirID'] = dirID
    return result

  def __getNumericPath(self, dirID, connection=False):
    """ Get the enumerated path of the given directory
    """
    epathString = ','.join(['LPATH%d' % (i + 1) for i in range(MAX_LEVELS)])
    req = 'SELECT LEVEL,%s FROM FC_DirectoryLevelTree WHERE DirID=%d' % (epathString, dirID)
    result = self.db._query(req, connection)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_OK([])

    row = result['Value'][0]
    level = row[0]
    epathList = []
    for i in range(level):
      epathList.append(row[i + 1])

    result = S_OK(epathList)
    result['Level'] = level
    return result

  def makeDir(self, path):
    """ Create a new directory entry
    """
    result = self.findDir(path)
    if not result['OK']:
      return result
    dirID = result['Value']
    if dirID:
      result = S_OK(dirID)
      result['NewDirectory'] = False
      return result

    dpath = path
    if path == '/':
      level = 0
      elements = []
      parentDirID = 0
    else:
      if path[0] == "/":
        dpath = path[1:]
      elements = dpath.split('/')
      level = len(elements)
      if level > MAX_LEVELS:
        return S_ERROR('Too many directory levels: %d' % level)
      result = self.getParent(path)
      if not result['OK']:
        return result
      parentDirID = result['Value']

    epathList = []
    if parentDirID:
      result = self.__getNumericPath(parentDirID)
      if not result['OK']:
        return result
      epathList = result['Value']

    names = ['DirName', 'Level', 'Parent']
    values = [path, level, parentDirID]
    if path != '/':
      for i in range(1, level, 1):
        names.append('LPATH%d' % i)
        values.append(epathList[i - 1])

    result = self.db._getConnection()
    conn = result['Value']
    # result = self.db._query("LOCK TABLES FC_DirectoryLevelTree WRITE; ",conn)
    result = self.db.insertFields('FC_DirectoryLevelTree', names, values, conn)
    if not result['OK']:
      # resUnlock = self.db._query("UNLOCK TABLES;",conn)
      if result['Message'].find('Duplicate') != -1:
        # The directory is already added
        resFind = self.findDir(path)
        if not resFind['OK']:
          return resFind
        dirID = resFind['Value']
        result = S_OK(dirID)
        result['NewDirectory'] = False
        return result
      else:
        return result
    dirID = result['lastRowId']

    # Update the path number
    if parentDirID:
      #       lPath = "LPATH%d" % (level)
      #       req = " SELECT @tmpvar:=max(%s)+1 FROM FC_DirectoryLevelTree WHERE Parent=%d; " % (lPath,parentDirID)
      #       resultLock = self.db._query("LOCK TABLES FC_DirectoryLevelTree WRITE; ",conn)
      #       result = self.db._query(req,conn)
      #       req = "UPDATE FC_DirectoryLevelTree SET %s=@tmpvar WHERE DirID=%d; " % (lPath,dirID)
      #       result = self.db._update(req,conn)
      #       result = self.db._query("UNLOCK TABLES;",conn)
      lPath = "LPATH%d" % (level)
      req = " SELECT @tmpvar:=max(%s)+1 FROM FC_DirectoryLevelTree WHERE Parent=%d FOR UPDATE; " % (lPath, parentDirID)
      resultLock = self.db._query("START TRANSACTION; ", conn)
      result = self.db._query(req, conn)
      req = "UPDATE FC_DirectoryLevelTree SET %s=@tmpvar WHERE DirID=%d; " % (lPath, dirID)
      result = self.db._update(req, conn)
      result = self.db._query("COMMIT;", conn)
      if not result['OK']:
        return result
    else:
      result = self.db._query("ROLLBACK;", conn)

    result = S_OK(dirID)
    result['NewDirectory'] = True
    return result

  def existsDir(self, path):
    """ Check the existence of a directory at the specified path
    """
    result = self.findDir(path)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_OK({"Exists": False})
    return S_OK({"Exists": True, "DirID": result['Value']})

  def getParent(self, path):
    """ Get the parent ID of the given directory
    """

    parent_dir = os.path.dirname(path)
    return self.findDir(parent_dir)

  def getParentID(self, dirPathOrID):
    """ Get the ID of the parent of a directory specified by ID
    """

    dirID = dirPathOrID
    if isinstance(dirPathOrID, six.string_types):
      result = self.findDir(dirPathOrID)
      if not result['OK']:
        return result
      dirID = result['Value']

    if dirID == 0:
      return S_ERROR('Root directory ID given')

    req = "SELECT Parent FROM FC_DirectoryLevelTree WHERE DirID=%d" % dirID
    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_ERROR('No parent found')

    return S_OK(result['Value'][0][0])

  def getDirectoryPath(self, dirID):
    """ Get directory name by directory ID
    """
    req = "SELECT DirName FROM FC_DirectoryLevelTree WHERE DirID=%d" % int(dirID)
    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_ERROR('Directory with id %d not found' % int(dirID))

    return S_OK(result['Value'][0][0])

  def getDirectoryPaths(self, dirIDList):
    """ Get directory name by directory ID list
    """
    dirs = dirIDList
    if not isinstance(dirIDList, list):
      dirs = [dirIDList]

    if not dirs:
      return S_OK({})

    dirListString = ','.join([str(d) for d in dirs])
    req = "SELECT DirID,DirName FROM FC_DirectoryLevelTree WHERE DirID in ( %s )" % dirListString
    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_ERROR('Directories not found: %s' % dirListString)

    resultDict = {}
    for row in result['Value']:
      resultDict[int(row[0])] = row[1]

    return S_OK(resultDict)

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

    result = self.getDirectoryPath(dirID)
    if not result['OK']:
      return result

    return S_OK(os.path.basename(result['Value']))

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

    elements = path.split('/')
    pelements = []
    dPath = ''
    for el in elements[1:]:
      dPath += '/' + el
      pelements.append(dPath)
    pelements.append('/')

    pathString = ["'" + p + "'" for p in pelements]
    req = "SELECT DirID FROM FC_DirectoryLevelTree WHERE DirName in (%s) ORDER BY DirID" % ','.join(pathString)
    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_ERROR('Directory %s not found' % path)

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

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

    # The method should be rather implemented using enumerated paths

    result = self.getDirectoryPath(dirID)
    if not result['OK']:
      return result
    dPath = result['Value']
    return self.getPathIDs(dPath)

  def getPathIDsByID(self, dirID):
    """ Get IDs of all the directories in the parent hierarchy for a directory
        specified by its ID
    """
    result = self.__getNumericPath(dirID)
    if not result['OK']:
      return result
    level = result['Level']
    if level == 0:
      return S_OK([dirID])
    lpaths = result['Value']

    lpathSelects = []
    for lev in range(level):
      sel = ' AND '.join(["Level=%d" % lev] + ['LPATH%d=%d' % (ll + 1, lpaths[ll]) for ll in range(lev)])
      lpathSelects.append(sel)
    selection = '(' + ') OR ('.join(lpathSelects) + ')'
    req = "SELECT Level,DirID from FC_DirectoryLevelTree WHERE %s ORDER BY Level" % selection
    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_ERROR('No result for the path of Directory with ID %d' % dirID)

    return S_OK([x[1] for x in result['Value']] + [dirID])

  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
    req = "SELECT DirID FROM FC_DirectoryLevelTree WHERE Parent=%d" % dirID
    result = self.db._query(req, connection)
    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
    """
    req = "SELECT Level FROM FC_DirectoryLevelTree WHERE DirID=%d" % dirID
    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_ERROR('Directory %d not found' % dirID)
    level = result['Value'][0][0]

    sPaths = []
    if requestString:
      req = "SELECT DirID FROM FC_DirectoryLevelTree"
    else:
      req = "SELECT Level,DirID FROM FC_DirectoryLevelTree"
    if level > 0:
      req += " AS F1"
      for i in range(1, level + 1):
        sPaths.append('LPATH%d' % i)
      pathString = ','.join(sPaths)
      req += " JOIN (SELECT %s FROM FC_DirectoryLevelTree WHERE DirID=%d) AS F2 ON " % (pathString, dirID)
      sPaths = []
      for i in range(1, level + 1):
        sPaths.append('F1.LPATH%d=F2.LPATH%d' % (i, i))
      pString = ' AND '.join(sPaths)
      if includeParent:
        req += "%s AND F1.Level >= %d" % (pString, level)
      else:
        req += "%s AND F1.Level > %d" % (pString, level)

    if requestString:
      return S_OK(req)

    result = self.db._query(req)
    if not result['OK']:
      return result
    if not result['Value']:
      return S_OK({})

    resDict = {}
    for row in result['Value']:
      resDict[row[1]] = row[0]

    return S_OK(resDict)

  def countSubdirectories(self, dirId, includeParent=True):
    result = self.getSubdirectoriesByID(dirId, requestString=True, includeParent=includeParent)
    if not result['OK']:
      return result
    reqDir = result['Value'].replace('SELECT DirID FROM', 'SELECT count(*) FROM')

    result = self.db._query(reqDir)
    if not result['OK']:
      return result
    return S_OK(result['Value'][0][0])

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

    dirs = dirList
    if not isinstance(dirList, list):
      dirs = [dirList]
    resultList = []
    parentList = dirs
    while parentList:
      subResult = []
      dirListString = ','.join([str(d) for d in parentList])
      req = 'SELECT DirID from FC_DirectoryLevelTree WHERE Parent in ( %s )' % dirListString
      result = self.db._query(req)
      if not result['OK']:
        return result
      for row in result['Value']:
        subResult.append(row[0])
      if subResult:
        resultList += subResult
      parentList = subResult

    return S_OK(resultList)

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

    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 recoverOrphanDirectories(self, credDict):
    """ Recover orphan directories
    """
    # Find out orphan directories
    treeTable = 'FC_DirectoryLevelTree'
    req = "SELECT DirID,Parent,Level FROM %s WHERE Parent NOT IN ( SELECT DirID from %s )" % (treeTable, treeTable)
    result = self.db._query(req)
    if not result['OK']:
      return result

    parentDict = {}
    for dirID, parentID, _level in result['Value']:

      result = self.getDirectoryPath(dirID)
      if not result['OK']:
        continue
      dirPath = result['Value']
      parentPath = os.path.dirname(dirPath)
      if not dirPath == '/':
        parentDict.setdefault(parentPath, {})
        parentDict[parentPath].setdefault('DirList', [])
        parentDict[parentPath]['DirList'].append(dirID)
        parentDict[parentPath]['OldParentID'] = parentID

    for parentPath, dirDict in parentDict.items():
      dirIDList = dirDict['DirList']
      oldParentID = dirDict['OldParentID']
      result = self.findDir(parentPath)
      if not result['OK']:
        continue
      if result['Value']:
        # The parent directory was recreated already
        parentID = result['Value']
      else:
        # The parent directory was lost
        result = self.makeDirectories(parentPath, credDict)
        if not result['OK']:
          continue
        parentID = result['Value']
        # We have created a new directory but let's keep the old ID
        req = "UPDATE FC_DirectoryLevelTree SET DirID=%s WHERE DirID=%s" % (oldParentID, parentID)
        result = self.db._update(req)
        if not result['OK']:
          continue
        req = "UPDATE FC_DirectoryInfo SET DirID=%s WHERE DirID=%s" % (oldParentID, parentID)
        result = self.db._update(req)

        parentID = oldParentID
        # We have to change also the ownership of the new directory to the most likely one
        # which is the owner of the containing directory
        containerPath = os.path.dirname(parentPath)
        result = self.getDirectoryParameters(containerPath)
        if result['OK']:
          conDict = result['Value']
          uid = conDict['UID']
          gid = conDict['GID']
          result = self._setDirectoryParameter(parentID, 'UID', uid)
          result = self._setDirectoryParameter(parentID, 'GID', gid)

      dirString = ','.join([str(dirID) for dirID in dirIDList])
      req = "UPDATE FC_DirectoryLevelTree SET Parent=%s WHERE DirID IN (%s)" % (parentID, dirString)
      result = self.db._update(req)
      if not result['OK']:
        continue

      connection = self._getConnection()
      result = self.db._query("LOCK TABLES FC_DirectoryLevelTree WRITE", connection)
      if not result['OK']:
        resUnlock = self.db._query("UNLOCK TABLES", connection)
        return result
      result = self.__rebuildLevelIndexes(parentID, connection)
      resUnlock = self.db._query("UNLOCK TABLES", connection)

    return S_OK()

  def _getConnection(self, connection=False):
    if connection:
      return connection
    res = self.db._getConnection()
    if res['OK']:
      return res['Value']
    return connection

  def __rebuildLevelIndexes(self, parentID, connection=False):
    """ Rebuild level indexes for all the subdirectories
    """
    result = self.__getNumericPath(parentID, connection)
    if not result['OK']:
      return result

    parentIndexList = result['Value']
    parentLevel = result['Level']
    result = self.getChildren(parentID, connection)
    if not result['OK']:
      return result
    subIDList = result['Value']

    indexList = list(parentIndexList)
    indexList.append(0)
    for dirID in subIDList:
      indexList[-1] += 1
      lpaths = ['LPATH%d=%d' % (i + 1, indexList[i]) for i in range(parentLevel + 1)]
      lpathString = 'SET ' + ','.join(lpaths)
      req = "UPDATE FC_DirectoryLevelTree %s WHERE DirID=%s" % (lpathString, dirID)
      result = self.db._update(req, connection)
      if not result['OK']:
        return result
      result = self.__rebuildLevelIndexes(dirID, connection)

    return S_OK()
