
from __future__ import absolute_import

import logging
import weakref

from vdsm.common import exception
from vdsm.storage import blockSD
from vdsm.storage import clusterlock
from vdsm.storage import exception as se
from vdsm.storage import misc
from vdsm.storage import sd
from vdsm.storage.persistent import DictValidator
from vdsm.storage.persistent import unicodeDecoder
from vdsm.storage.persistent import unicodeEncoder
from vdsm.storage.securable import secured
from vdsm.storage.securable import unsecured
from vdsm.storage.sp import LVER_INVALID
from vdsm.storage.sp import SPM_ACQUIRED
from vdsm.storage.sp import SPM_FREE
from vdsm.storage.sp import SPM_ID_FREE
from vdsm.config import config


MAX_POOL_DESCRIPTION_SIZE = 50

PMDK_DOMAINS = "POOL_DOMAINS"
PMDK_POOL_DESCRIPTION = "POOL_DESCRIPTION"
PMDK_LVER = "POOL_SPM_LVER"
PMDK_SPM_ID = "POOL_SPM_ID"
PMDK_MASTER_VER = "MASTER_VERSION"


MAX_DOMAINS = blockSD.SD_METADATA_SIZE - blockSD.METADATA_BASE_SIZE
MAX_DOMAINS -= MAX_POOL_DESCRIPTION_SIZE + sd.MAX_DOMAIN_DESCRIPTION_SIZE
MAX_DOMAINS -= blockSD.PVS_METADATA_SIZE
MAX_DOMAINS //= 48


def _domainListEncoder(domDict):
    domains = ','.join(['%s:%s' % (k, v) for k, v in domDict.iteritems()])
    return domains


def _domainListDecoder(s):
    domList = {}
    if not s:
        return domList
    for domDecl in s.split(","):
        k, v = domDecl.split(':')
        domList[k.strip("'")] = v.strip("'").capitalize()
    return domList


SP_MD_FIELDS = {
    PMDK_DOMAINS: (_domainListDecoder, _domainListEncoder),
    PMDK_POOL_DESCRIPTION: (unicodeDecoder, unicodeEncoder),
    PMDK_LVER: (int, str),
    PMDK_SPM_ID: (int, str),
    PMDK_MASTER_VER: (int, str)
}


@secured
class StoragePoolBackendInterface(object):

    def __is_secure__(self):
        return False

    @unsecured
    def getSpmStatus(self):
        raise NotImplementedError()

    def setSpmStatus(self, lVer=None, spmId=None):
        raise NotImplementedError()

    @unsecured
    def getDomainsMap(self):
        raise NotImplementedError()

    def setDomainsMap(self, domains):
        raise NotImplementedError()

    @unsecured
    def getMaximumSupportedDomains(self):
        raise NotImplementedError()

    @unsecured
    def getMasterVersion(self):
        raise NotImplementedError()

    @unsecured
    def validateMasterDomainVersion(self, masterDomain, masterVersion):
        raise NotImplementedError()

    def setDomainRegularRole(self, domain):
        raise NotImplementedError()

    @unsecured
    def initParameters(self, poolName, domain, masterVersion):
        raise NotImplementedError()

    def switchMasterDomain(self, curMasterDomain, newMasterDomain,
                           newMasterVersion):
        raise NotImplementedError()

    @unsecured
    def getInfo(self):
        raise NotImplementedError()


@secured
class StoragePoolDiskBackend(StoragePoolBackendInterface):

    __slots__ = ('pool',)

    log = logging.getLogger('storage.StoragePoolDiskBackend')

    def __init__(self, pool):
        self.pool = weakref.proxy(pool)


    def __is_secure__(self):
        return self.pool.isSecure()

    @property
    def id(self):
        return self.pool.id

    @property
    def spmRole(self):
        return self.pool.spmRole

    @property
    def spUUID(self):
        return self.pool.spUUID

    @property
    def masterDomain(self):
        return self.pool.masterDomain


    @unsecured
    def getSpmStatus(self):
        poolMeta = self._getPoolMD(self.masterDomain)

        if (poolMeta[PMDK_SPM_ID] == self.id and
                not self.spmRole == SPM_ACQUIRED):
            self.invalidateMetadata()
            poolMeta = self._getPoolMD(self.masterDomain)

        return poolMeta[PMDK_LVER], poolMeta[PMDK_SPM_ID]

    def setSpmStatus(self, lVer=None, spmId=None):
        self.invalidateMetadata()
        metaParams = {}
        if lVer is not None:
            metaParams[PMDK_LVER] = lVer
        if spmId is not None:
            metaParams[PMDK_SPM_ID] = spmId
        self._metadata.update(metaParams)

    @unsecured
    def getDomainsMap(self):
        self.invalidateMetadata()
        return self.getMetaParam(PMDK_DOMAINS)

    def setDomainsMap(self, domains):
        self.setMetaParam(PMDK_DOMAINS, domains)

    @unsecured
    def getMaximumSupportedDomains(self):
        msdInfo = self.masterDomain.getInfo()
        msdType = sd.name2type(msdInfo["type"])
        msdVersion = int(msdInfo["version"])
        if msdType in sd.BLOCK_DOMAIN_TYPES and \
                msdVersion in blockSD.VERS_METADATA_LV:
            return MAX_DOMAINS
        else:
            return config.getint("irs", "maximum_domains_in_pool")

    @unsecured
    def getMasterVersion(self):
        return self.getMetaParam(PMDK_MASTER_VER)

    @unsecured
    def validateMasterDomainVersion(self, masterDomain, masterVersion):
        version = self._getPoolMD(masterDomain)[PMDK_MASTER_VER]
        if version != int(masterVersion):
            self.log.error("Requested master domain %s does not have expected "
                           "version %s it is version %s",
                           masterDomain.sdUUID, masterVersion, version)
            raise se.StoragePoolWrongMaster(self.spUUID, masterDomain.sdUUID)

    def setDomainRegularRole(self, domain):
        poolMetadata = self._getPoolMD(domain)
        with poolMetadata.transaction():
            poolMetadata[PMDK_MASTER_VER] = 0
            domain.changeRole(sd.REGULAR_DOMAIN)

    @unsecured
    def initParameters(self, poolName, domain, masterVersion):
        self._getPoolMD(domain).update({
            PMDK_SPM_ID: SPM_ID_FREE,
            PMDK_LVER: LVER_INVALID,
            PMDK_MASTER_VER: masterVersion,
            PMDK_POOL_DESCRIPTION: poolName,
            PMDK_DOMAINS: {domain.sdUUID: sd.DOM_ACTIVE_STATUS},
        })

    def switchMasterDomain(self, curMasterDomain, newMasterDomain,
                           newMasterVersion):
        curPoolMD = self._getPoolMD(curMasterDomain)
        newPoolMD = self._getPoolMD(newMasterDomain)

        newPoolMD.update({
            PMDK_DOMAINS: curPoolMD[PMDK_DOMAINS],
            PMDK_POOL_DESCRIPTION: curPoolMD[PMDK_POOL_DESCRIPTION],
            PMDK_LVER: curPoolMD[PMDK_LVER],
            PMDK_SPM_ID: curPoolMD[PMDK_SPM_ID],
            PMDK_MASTER_VER: newMasterVersion,
        })

    @unsecured
    def getInfo(self):
        try:
            pmd = self._getPoolMD(self.masterDomain)
        except Exception:
            self.log.error("Pool metadata error", exc_info=True)
            raise se.StoragePoolActionError(self.spUUID)

        return {
            'name': pmd[PMDK_POOL_DESCRIPTION],
            'domains': _domainListEncoder(pmd[PMDK_DOMAINS]),
            'master_ver': pmd[PMDK_MASTER_VER],
            'lver': pmd[PMDK_LVER],
            'spm_id': pmd[PMDK_SPM_ID],
        }


    @unsecured
    def forceFreeSpm(self):
        self.setSpmStatus(LVER_INVALID, SPM_ID_FREE, __securityOverride=True)
        self.pool.spmRole = SPM_FREE

    @classmethod
    def _getPoolMD(cls, domain):
        return DictValidator(domain._metadata._dict, SP_MD_FIELDS)

    @property
    def _metadata(self):
        return self._getPoolMD(self.masterDomain)

    @unsecured
    def getMetaParam(self, key):
        return self._metadata[key]

    def setMetaParam(self, key, value):
        self._metadata[key] = value

    @unsecured
    def getDescription(self):
        try:
            return self.getMetaParam(PMDK_POOL_DESCRIPTION)
        except KeyError:
            return ""

    def setDescription(self, descr):
        if len(descr) > MAX_POOL_DESCRIPTION_SIZE:
            raise se.StoragePoolDescriptionTooLongError()

        self.log.info("spUUID=%s descr=%s", self.spUUID, descr)

        if not misc.isAscii(descr) and not self.masterDomain.supportsUnicode():
            raise se.UnicodeArgumentException()

        self.setMetaParam(PMDK_POOL_DESCRIPTION, descr)

    @unsecured
    def invalidateMetadata(self):
        if not self.spmRole == SPM_ACQUIRED:
            self._metadata.invalidate()


@secured
class StoragePoolMemoryBackend(StoragePoolBackendInterface):

    __slots__ = ('pool', 'masterVersion', 'domainsMap')

    log = logging.getLogger('storage.StoragePoolMemoryBackend')

    def __init__(self, pool, masterVersion, domainsMap):
        self.pool = weakref.proxy(pool)
        self.updateVersionAndDomains(masterVersion, domainsMap)


    def __is_secure__(self):
        return self.pool.isSecure()

    @property
    def spUUID(self):
        return self.pool.spUUID

    @property
    def masterDomain(self):
        return self.pool.masterDomain


    @unsecured
    def getSpmStatus(self):
        try:
            lVer, spmId = self.masterDomain.inquireClusterLock()
        except clusterlock.TemporaryFailure as e:
            raise exception.expected(e)
        return lVer or LVER_INVALID, spmId or SPM_ID_FREE

    def setSpmStatus(self, lVer, spmId):
        self.log.debug(
            'this storage pool implementation ignores the set spm '
            'status requests (lver=%s, spmid=%s)', lVer, spmId)

    @unsecured
    def getDomainsMap(self):
        return self.domainsMap

    def setDomainsMap(self, domainsMap):
        self.domainsMap = dict(
            ((k, v.capitalize()) for k, v in domainsMap.iteritems()))
        self.log.info(
            'new storage pool master version %s and domains map %s',
            self.masterVersion, self.domainsMap)

    @unsecured
    def getMaximumSupportedDomains(self):
        return config.getint("irs", "maximum_domains_in_pool")

    @unsecured
    def getMasterVersion(self):
        return self.masterVersion

    @unsecured
    def validateMasterDomainVersion(self, masterDomain, masterVersion):
        if self.masterVersion != int(masterVersion):
            self.log.error(
                'requested master version %s is not the expected one %s',
                masterVersion, self.masterVersion)
            raise se.StoragePoolWrongMaster(self.spUUID, masterDomain.sdUUID)

    def setDomainRegularRole(self, domain):
        domain.changeRole(sd.REGULAR_DOMAIN)

    @unsecured
    def initParameters(self, domain, poolName, masterVersion):
        self.log.debug(
            'this storage pool implementation ignores master '
            'domain initialization (sdUUID=%s, poolName="%s", '
            'masterVersion=%s)', domain.sdUUID, poolName, masterVersion)

    def switchMasterDomain(self, currentMasterDomain, newMasterDomain,
                           newMasterVersion):
        self.log.debug(
            'switching from master domain %s version %s to master domain '
            '%s version %s', currentMasterDomain.sdUUID, self.masterVersion,
            newMasterDomain.sdUUID, newMasterVersion)
        self.masterVersion = newMasterVersion

    @unsecured
    def getInfo(self):
        lVer, spmId = self.getSpmStatus()
        return {
            'name': 'No Description',
            'domains': _domainListEncoder(self.domainsMap),
            'master_ver': self.masterVersion,
            'lver': lVer,
            'spm_id': spmId,
        }


    @unsecured
    def updateVersionAndDomains(self, masterVersion, domainsMap):
        self.log.debug('updating domain version to %s and domains map '
                       'to %s', masterVersion, domainsMap)
        self.masterVersion = masterVersion
        self.setDomainsMap(domainsMap, __securityOverride=True)
