""" Base Storage Class provides the base interface for all storage plug-ins

      exists()

These are the methods for manipulating files:
      isFile()
      getFile()
      putFile()
      removeFile()
      getFileMetadata()
      getFileSize()
      prestageFile()
      getTransportURL()

These are the methods for manipulating directories:
      isDirectory()
      getDirectory()
      putDirectory()
      createDirectory()
      removeDirectory()
      listDirectory()
      getDirectoryMetadata()
      getDirectorySize()

These are the methods for manipulating the client:
      changeDirectory()
      getCurrentDirectory()
      getName()
      getParameters()
      getCurrentURL()

These are the methods for getting information about the Storage:
      getOccupancy()

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
__RCSID__ = "$Id$"

import errno
import json
import os
import shutil
import tempfile

from DIRAC import S_OK, S_ERROR
from DIRAC.Core.Utilities.Pfn import pfnparse, pfnunparse
from DIRAC.Core.Utilities.ReturnValues import returnSingleResult
from DIRAC.Resources.Storage.Utilities import checkArgumentFormat


class StorageBase(object):
  """
  .. class:: StorageBase

  """

  PROTOCOL_PARAMETERS = ["Protocol", "Host", "Path", "Port", "SpaceToken", "WSUrl"]
  # Options to be prepended in the URL
  # keys are the name of the parameters in the CS
  # values are the name of the options as they appear in the URL
  DYNAMIC_OPTIONS = {}

  def __init__(self, name, parameterDict):

    self.name = name
    self.pluginName = ''
    self.protocolParameters = {}

    self.__updateParameters(parameterDict)

    # Keep the list of all parameters passed for constructions
    # Taken from the CS
    # In a further major release, this could be nerged together
    # with protocolParameters. There is no reason for it to
    # be so strict about the possible content.
    self._allProtocolParameters = parameterDict

    if hasattr(self, '_INPUT_PROTOCOLS'):
      self.protocolParameters['InputProtocols'] = getattr(self, '_INPUT_PROTOCOLS')
    else:
      self.protocolParameters['InputProtocols'] = [self.protocolParameters['Protocol'], 'file']

    if hasattr(self, '_OUTPUT_PROTOCOLS'):
      self.protocolParameters['OutputProtocols'] = getattr(self, '_OUTPUT_PROTOCOLS')
    else:
      self.protocolParameters['OutputProtocols'] = [self.protocolParameters['Protocol']]

    self.basePath = parameterDict['Path']
    self.cwd = self.basePath
    self.se = None
    self.isok = True

    # use True for backward compatibility
    self.srmSpecificParse = True

  def setStorageElement(self, se):
    self.se = se

  def setParameters(self, parameterDict):
    """ Set standard parameters, method can be overriden in subclasses
        to process specific parameters
    """
    self.__updateParameters(parameterDict)

  def __updateParameters(self, parameterDict):
    """ setParameters implementation method
    """
    for item in self.PROTOCOL_PARAMETERS:
      self.protocolParameters[item] = parameterDict.get(item, '')

  def getParameters(self):
    """ Get the parameters with which the storage was instantiated
    """
    parameterDict = dict(self.protocolParameters)
    parameterDict["StorageName"] = self.name
    parameterDict["PluginName"] = self.pluginName
    parameterDict['URLBase'] = self.getURLBase().get('Value', '')
    parameterDict['Endpoint'] = self.getEndpoint().get('Value', '')

    return parameterDict

  def exists(self, *parms, **kws):
    """Check if the given path exists
    """
    return S_ERROR("Storage.exists: implement me!")

  #############################################################
  #
  # These are the methods for file manipulation
  #

  def isFile(self, *parms, **kws):
    """Check if the given path exists and it is a file
    """
    return S_ERROR("Storage.isFile: implement me!")

  def getFile(self, *parms, **kws):
    """Get a local copy of the file specified by its path
    """
    return S_ERROR("Storage.getFile: implement me!")

  def putFile(self, *parms, **kws):
    """Put a copy of the local file to the current directory on the
       physical storage
    """
    return S_ERROR("Storage.putFile: implement me!")

  def removeFile(self, *parms, **kws):
    """Remove physically the file specified by its path
    """
    return S_ERROR("Storage.removeFile: implement me!")

  def getFileMetadata(self, *parms, **kws):
    """  Get metadata associated to the file
    """
    return S_ERROR("Storage.getFileMetadata: implement me!")

  def getFileSize(self, *parms, **kws):
    """Get the physical size of the given file
    """
    return S_ERROR("Storage.getFileSize: implement me!")

  def prestageFile(self, *parms, **kws):
    """ Issue prestage request for file
    """
    return S_ERROR("Storage.prestageFile: implement me!")

  def prestageFileStatus(self, *parms, **kws):
    """ Obtain the status of the prestage request
    """
    return S_ERROR("Storage.prestageFileStatus: implement me!")

  def pinFile(self, *parms, **kws):
    """ Pin the file on the destination storage element
    """
    return S_ERROR("Storage.pinFile: implement me!")

  def releaseFile(self, *parms, **kws):
    """ Release the file on the destination storage element
    """
    return S_ERROR("Storage.releaseFile: implement me!")

  #############################################################
  #
  # These are the methods for directory manipulation
  #

  def isDirectory(self, *parms, **kws):
    """Check if the given path exists and it is a directory
    """
    return S_ERROR("Storage.isDirectory: implement me!")

  def getDirectory(self, *parms, **kws):
    """Get locally a directory from the physical storage together with all its
       files and subdirectories.
    """
    return S_ERROR("Storage.getDirectory: implement me!")

  def putDirectory(self, *parms, **kws):
    """Put a local directory to the physical storage together with all its
       files and subdirectories.
    """
    return S_ERROR("Storage.putDirectory: implement me!")

  def createDirectory(self, *parms, **kws):
    """ Make a new directory on the physical storage
    """
    return S_ERROR("Storage.createDirectory: implement me!")

  def removeDirectory(self, *parms, **kws):
    """Remove a directory on the physical storage together with all its files and
       subdirectories.
    """
    return S_ERROR("Storage.removeDirectory: implement me!")

  def listDirectory(self, *parms, **kws):
    """ List the supplied path
    """
    return S_ERROR("Storage.listDirectory: implement me!")

  def getDirectoryMetadata(self, *parms, **kws):
    """ Get the metadata for the directory
    """
    return S_ERROR("Storage.getDirectoryMetadata: implement me!")

  def getDirectorySize(self, *parms, **kws):
    """ Get the size of the directory on the storage
    """
    return S_ERROR("Storage.getDirectorySize: implement me!")

  #############################################################
  #
  # These are the methods for manipulating the client
  #

  def isOK(self):
    return self.isok

  def resetCurrentDirectory(self):
    """ Reset the working directory to the base dir
    """
    self.cwd = self.basePath

  def changeDirectory(self, directory):
    """ Change the directory to the supplied directory
    """
    if directory.startswith('/'):
      self.cwd = "%s/%s" % (self.basePath, directory)
    else:
      self.cwd = '%s/%s' % (self.cwd, directory)

  def getCurrentDirectory(self):
    """ Get the current directory
    """
    return self.cwd

  def getCurrentURL(self, fileName):
    """ Obtain the current file URL from the current working directory and the filename

    :param self: self reference
    :param str fileName: path on storage
    """
    urlDict = dict(self.protocolParameters)
    if not fileName.startswith('/'):
      # Relative path is given
      urlDict['Path'] = self.cwd
    result = pfnunparse(urlDict, srmSpecific=self.srmSpecificParse)
    if not result['OK']:
      return result
    cwdUrl = result['Value']
    fullUrl = '%s%s' % (cwdUrl, fileName)
    return S_OK(fullUrl)

  def getName(self):
    """ The name with which the storage was instantiated
    """
    return self.name

  def getURLBase(self, withWSUrl=False):
    """ This will get the URL base. This is then appended with the LFN in DIRAC convention.

    :param self: self reference
    :param bool withWSUrl: flag to include Web Service part of the url
    :returns: URL
    """
    urlDict = dict(self.protocolParameters)
    if not withWSUrl:
      urlDict['WSUrl'] = ''
    return pfnunparse(urlDict, srmSpecific=self.srmSpecificParse)

  def getEndpoint(self):
    """ This will get endpoint of the storage. It basically is the same as :py:meth:`getURLBase`
        but without the basePath

    :returns: 'proto://hostname<:port>'

    """
    urlDict = dict(self.protocolParameters)
    # We remove the basePath
    urlDict['Path'] = ''
    return pfnunparse(urlDict, srmSpecific=self.srmSpecificParse)

  def isURL(self, path):
    """ Guess if the path looks like a URL

    :param self: self reference
    :param string path: input file LFN or URL
    :returns boolean: True if URL, False otherwise
    """
    if self.basePath and path.startswith(self.basePath):
      return S_OK(True)

    result = pfnparse(path, srmSpecific=self.srmSpecificParse)
    if not result['OK']:
      return result

    if len(result['Value']['Protocol']) != 0:
      return S_OK(True)

    if result['Value']['Path'].startswith(self.basePath):
      return S_OK(True)

    return S_OK(False)

  def getTransportURL(self, pathDict, protocols):
    """ Get a transport URL for a given URL. For a simple storage plugin
    it is just returning input URL if the plugin protocol is one of the
    requested protocols

    :param dict pathDict: URL obtained from File Catalog or constructed according
                    to convention
    :param protocols: a list of acceptable transport protocols in priority order
    :type protocols: `python:list`
    """
    res = checkArgumentFormat(pathDict)
    if not res['OK']:
      return res
    urls = res['Value']
    successful = {}
    failed = {}

    if protocols and not self.protocolParameters['Protocol'] in protocols:
      return S_ERROR(errno.EPROTONOSUPPORT, 'No native protocol requested')

    for url in urls:
      successful[url] = url

    resDict = {'Failed': failed, 'Successful': successful}
    return S_OK(resDict)

  def constructURLFromLFN(self, lfn, withWSUrl=False):
    """ Construct URL from the given LFN according to the VO convention for the
    primary protocol of the storage plagin

    :param str lfn: file LFN
    :param boolean withWSUrl: flag to include the web service part into the resulting URL
    :return result: result['Value'] - resulting URL
    """

    # Check the LFN convention:
    # 1. LFN must start with the VO name as the top level directory
    # 2. VO name must not appear as any subdirectory or file name
    lfnSplitList = lfn.split('/')
    voLFN = lfnSplitList[1]
    # TODO comparison to Sandbox below is for backward compatibility, should
    # be removed in the next release
    if voLFN != self.se.vo and voLFN != "SandBox" and voLFN != "Sandbox":
      return S_ERROR('LFN (%s) path must start with VO name (%s)' % (lfn, self.se.vo))

    urlDict = dict(self.protocolParameters)
    urlDict['Options'] = '&'.join("%s=%s" % (optionName, urlDict[paramName])
                                  for paramName, optionName in self.DYNAMIC_OPTIONS.items()
                                  if urlDict.get(paramName))
    if not withWSUrl:
      urlDict['WSUrl'] = ''
    urlDict['FileName'] = lfn.lstrip('/')

    return pfnunparse(urlDict, srmSpecific=self.srmSpecificParse)

  def updateURL(self, url, withWSUrl=False):
    """ Update the URL according to the current SE parameters
    """
    result = pfnparse(url, srmSpecific=self.srmSpecificParse)
    if not result['OK']:
      return result
    urlDict = result['Value']

    urlDict['Protocol'] = self.protocolParameters['Protocol']
    urlDict['Host'] = self.protocolParameters['Host']
    urlDict['Port'] = self.protocolParameters['Port']
    urlDict['WSUrl'] = ''
    if withWSUrl:
      urlDict['WSUrl'] = self.protocolParameters['WSUrl']

    return pfnunparse(urlDict, srmSpecific=self.srmSpecificParse)

  def isNativeURL(self, url):
    """ Check if URL :url: is valid for :self.protocol:

    :param self: self reference
    :param str url: URL
    """
    res = pfnparse(url, srmSpecific=self.srmSpecificParse)
    if not res['OK']:
      return res
    urlDict = res['Value']
    return S_OK(urlDict['Protocol'] == self.protocolParameters['Protocol'])

  @staticmethod
  def _addCommonMetadata(metadataDict):
    """ To make the output of getFileMetadata uniform throughout the protocols
        this returns a minimum set of metadata with default value,
        that are then complemented with the protocol specific metadata

        :param metadataDict: specific metadata of the protocol

        :returns: dictionnary with all the metadata (specific and basic)
    """
    commonMetadata = {'Checksum': '',
                      'Directory': False,
                      'File': False,
                      'Mode': 0o000,
                      'Size': 0,
                      'Accessible': True,
                      }

    commonMetadata.update(metadataDict)

    return commonMetadata

  def _isInputURL(self, url):
    """ Check if the given url can be taken as input

    :param self: self reference
    :param str url: URL
    """
    res = pfnparse(url)
    if not res['OK']:
      return res
    urlDict = res['Value']

    # Special case of 'file' protocol which can be just a URL
    if not urlDict['Protocol'] and 'file' in self.protocolParameters['InputProtocols']:
      return S_OK(True)

    return S_OK(urlDict['Protocol'] == self.protocolParameters['Protocol'])

  #############################################################
  #
  # These are the methods for getting information about the Storage element:
  #

  def getOccupancy(self, **kwargs):
    """ Get the StorageElement occupancy info in MB.

        This generic implementation download a json file supposed to contain the necessary info.

        :param occupancyLFN: (mandatory named argument) LFN of the json file.

        :returns: S_OK/S_ERROR dictionary. The S_OK value should contain a dictionary with Total and Free space in MB
    """

    # Build the URL for the occupancyLFN:
    occupancyLFN = kwargs['occupancyLFN']
    res = self.constructURLFromLFN(occupancyLFN)
    if not res['OK']:
      return res
    occupancyURL = res['Value']

    try:

      # download the file locally
      tmpDirName = tempfile.mkdtemp()
      res = returnSingleResult(self.getFile(occupancyURL, localPath=tmpDirName))
      if not res['OK']:
        return res

      filePath = os.path.join(tmpDirName, os.path.basename(occupancyLFN))

      # Read its json content
      with open(filePath, 'r') as occupancyFile:
        return S_OK(json.load(occupancyFile))

    except Exception as e:
      return S_ERROR(repr(e))

    finally:
      # Clean the temporary dir
      shutil.rmtree(tmpDirName)
