
from __future__ import absolute_import

import logging

from vdsm import constants
from vdsm.common import cmdutils
from vdsm.storage import blockSD
from vdsm.storage import constants as sc
from vdsm.storage import exception as se
from vdsm.storage import qemuimg
from vdsm.storage import sd

log = logging.getLogger("storage.format")


def __convertDomainMetadataToTags(domain, targetVersion):
    newMetadata = blockSD.TagBasedSDMetadata(domain.sdUUID)
    oldMetadata = domain._metadata

    metadata = oldMetadata._dict.copy()

    log.debug("Converting domain %s to tag based metadata", domain.sdUUID)
    newMetadata._dict.update(metadata)

    try:
        oldMetadata._dict.clear()
    except Exception as convertError:
        log.error("Could not clear the old metadata", exc_info=True)
    else:
        convertError = RuntimeError("Unknown metadata conversion error")

    chkMetadata = blockSD.selectMetadata(domain.sdUUID)

    if chkMetadata[sd.DMDK_VERSION] == int(targetVersion):
        domain.replaceMetadata(chkMetadata)
        log.debug("Conversion of domain %s to tag based metadata completed, "
                  "target version = %s", domain.sdUUID, targetVersion)
    else:
        log.error("Could not convert domain %s to tag based metadata, "
                  "target version = %s", domain.sdUUID, targetVersion)
        newMetadata._dict.clear()
        raise convertError


def v2DomainConverter(repoPath, hostId, domain, isMsd):
    targetVersion = 2

    if domain.getStorageType() in sd.BLOCK_DOMAIN_TYPES:
        log.debug("Trying to upgrade domain %s to tag based metadata "
                  "version %s", domain.sdUUID, targetVersion)

        __convertDomainMetadataToTags(domain, targetVersion)

    else:
        log.debug("Skipping the upgrade to tag based metadata version %s "
                  "for the domain %s", targetVersion, domain.sdUUID)


def _v3_reset_meta_volsize(vol):

    log.debug("Checking the volume size for the volume %s", vol.volUUID)

    meta_vol_size = int(vol.getMetaParam(sc.CAPACITY))

    if vol.getFormat() == sc.COW_FORMAT:
        qemuVolInfo = qemuimg.info(vol.getVolumePath(),
                                   qemuimg.FORMAT.QCOW2)
        virtual_vol_size = qemuVolInfo["virtualsize"]
    else:
        virtual_vol_size = vol.getVolumeSize()

    if meta_vol_size != virtual_vol_size:
        log.warn("Fixing the mismatch between the metadata volume size "
                 "(%s) and the volume virtual size (%s) for the volume "
                 "%s", meta_vol_size, virtual_vol_size, vol.volUUID)
        vol.setMetaParam(sc.CAPACITY, virtual_vol_size)


def v3DomainConverter(repoPath, hostId, domain, isMsd):
    targetVersion = 3
    currentVersion = domain.getVersion()

    log.debug("Starting conversion for domain %s from version %s "
              "to version %s", domain.sdUUID, currentVersion, targetVersion)

    targetVersion = 3
    currentVersion = domain.getVersion()

    if domain.getStorageType() in sd.BLOCK_DOMAIN_TYPES:
        if currentVersion == 0:
            log.debug("Upgrading domain %s from version %s to version 2",
                      domain.sdUUID, currentVersion)
            v2DomainConverter(repoPath, hostId, domain, isMsd)
            currentVersion = domain.getVersion()

        if currentVersion != 2:
            log.debug("Unsupported conversion from version %s to version %s",
                      currentVersion, targetVersion)
            raise se.UnsupportedDomainVersion(currentVersion)

    if domain.getStorageType() in sd.FILE_DOMAIN_TYPES:
        log.debug("Setting permissions for domain %s", domain.sdUUID)
        domain.setMetadataPermissions()

    log.debug("Initializing the new cluster lock for domain %s", domain.sdUUID)
    newClusterLock = domain._makeClusterLock(targetVersion)
    newClusterLock.initLock(domain.getClusterLease())

    log.debug("Acquiring the host id %s for domain %s", hostId, domain.sdUUID)
    newClusterLock.acquireHostId(hostId, wait=True)

    def v3UpgradeVolumePermissions(vol):
        log.debug("Changing permissions (read-write) for the "
                  "volume %s", vol.volUUID)
        vol._setrw(True)

    def v3ReallocateMetadataSlot(domain, allVolumes):
        if not domain.getStorageType() in sd.BLOCK_DOMAIN_TYPES:
            log.debug("The metadata reallocation check is not needed for "
                      "domain %s", domain.sdUUID)
            return

        leasesSize = domain.getLeasesFileSize() / constants.MEGAB
        metaMaxSlot = leasesSize - blockSD.RESERVED_LEASES - 1

        log.debug("Starting metadata reallocation check for domain %s with "
                  "metaMaxSlot %s (leases volume size %s)", domain.sdUUID,
                  metaMaxSlot, leasesSize)

        for volUUID, (imgUUIDs, parentUUID) in allVolumes.iteritems():
            vol = domain.produceVolume(imgUUIDs[0], volUUID)
            metaSlot = vol.getMetaSlot()

            if metaSlot < metaMaxSlot:
                continue

            log.debug("Reallocating metadata slot %s for volume %s",
                      metaSlot, vol.volUUID)
            metaContent = vol.getMetadata()

            with domain.acquireVolumeMetadataSlot(vol.volUUID) as newMetaSlot:
                if newMetaSlot > metaMaxSlot:
                    raise se.NoSpaceLeftOnDomain(domain.sdUUID)

                log.debug("Copying metadata for volume %s to the new slot %s",
                          vol.volUUID, newMetaSlot)
                vol.createMetadata((domain.sdUUID, newMetaSlot), metaContent)

                log.debug("Switching the metadata slot for volume %s to %s",
                          vol.volUUID, newMetaSlot)
                vol.changeVolumeTag(sc.TAG_PREFIX_MD, str(newMetaSlot))

    try:
        if isMsd:
            log.debug("Acquiring the cluster lock for domain %s with "
                      "host id: %s", domain.sdUUID, hostId)
            newClusterLock.acquire(hostId, domain.getClusterLease())

        allVolumes = domain.getAllVolumes()

        v3ReallocateMetadataSlot(domain, allVolumes)

        for volUUID, (imgUUIDs, parentUUID) in allVolumes.iteritems():
            log.debug("Converting volume: %s", volUUID)

            allImages.update((i, None) for i in imgUUIDs)

            vol = domain.produceVolume(imgUUIDs[0], volUUID)
            v3UpgradeVolumePermissions(vol)

            log.debug("Creating the volume lease for %s", volUUID)
            metaId = vol.getMetadataId()
            vol.newVolumeLease(metaId, domain.sdUUID, volUUID)

            for imgUUID in imgUUIDs[1:]:
                allImages[imgUUID] = imgUUIDs[0]
                dstVol = domain.produceVolume(imgUUID, volUUID)

                v3UpgradeVolumePermissions(dstVol)

                vol._shareLease(dstVol.imagePath)

        for imgUUID in allImages:
            log.debug("Converting image: %s", imgUUID)

            imgVolumes = sd.getVolsOfImage(allVolumes, imgUUID).keys()
            try:
                try:
                    domain.activateVolumes(imgUUID, imgVolumes)
                except (OSError, se.CannotActivateLogicalVolumes):
                    log.error("Image %s can't be activated.",
                              imgUUID, exc_info=True)

                for volUUID in imgVolumes:
                    try:
                        vol = domain.produceVolume(imgUUID, volUUID)
                    except cmdutils.Error:
                        log.error("It is not possible to read the volume %s "
                                  "using qemu-img, the content looks damaged",
                                  volUUID, exc_info=True)

            except se.VolumeDoesNotExist:
                log.error("It is not possible to prepare the image %s, the "
                          "volume chain looks damaged", imgUUID,
                          exc_info=True)

            except se.MetaDataKeyNotFoundError:
                log.error("It is not possible to prepare the image %s, the "
                          "volume metadata looks damaged", imgUUID,
                          exc_info=True)

            finally:
                try:
                    domain.deactivateImage(imgUUID)
                except se.CannotDeactivateLogicalVolume:
                    log.warning("Unable to teardown the image %s, this error "
                                "is not critical since the volume might be in"
                                " use", imgUUID, exc_info=True)

        log.debug("Finalizing the storage domain upgrade from version %s to "
                  "version %s for domain %s", currentVersion, targetVersion,
                  domain.sdUUID)
        domain.setMetaParam(sd.DMDK_VERSION, targetVersion)

    except:
        if isMsd:
            try:
                log.error("Releasing the cluster lock for domain %s with "
                          "host id: %s", domain.sdUUID, hostId)
                newClusterLock.release(domain.getClusterLease())
            except:
                log.error("Unable to release the cluster lock for domain "
                          "%s with host id: %s", domain.sdUUID, hostId,
                          exc_info=True)

        try:
            log.error("Releasing the host id %s for domain %s", hostId,
                      domain.sdUUID)
            newClusterLock.releaseHostId(hostId, wait=True, unused=True)
        except:
            log.error("Unable to release the host id %s for domain %s",
                      hostId, domain.sdUUID, exc_info=True)

        raise

    if isMsd:
        try:
            domain._clusterLock.release(domain.getClusterLease())
        except:
            log.error("Unable to release the old cluster lock for domain "
                      "%s ", domain.sdUUID, exc_info=True)

    log.debug("Switching the cluster lock for domain %s", domain.sdUUID)
    domain._clusterLock = newClusterLock


def v4DomainConverter(repoPath, hostId, domain, isMsd):
    targetVersion = 4

    log.debug("Starting conversion of domain %s to version %s",
              domain.sdUUID, targetVersion)

    if domain.supports_external_leases(targetVersion):
        domain.create_external_leases()

        xleases_path = domain.external_leases_path()

        domain.format_external_leases(
            domain.sdUUID,
            xleases_path,
            alignment=sc.ALIGNMENT_1M,
            block_size=sc.BLOCK_SIZE_512)

    domain.setMetaParam(sd.DMDK_VERSION, targetVersion)

    log.debug("Conversion of domain %s to version = %s has been completed.",
              domain.sdUUID, targetVersion)


def v5DomainConverter(repoPath, hostId, domain, isMsd):
    target_version = 5

    if domain.getVersion() == 3:
        v4DomainConverter(repoPath, hostId, domain, isMsd)

    log.debug("Starting conversion of domain %s to version %s",
              domain.sdUUID, target_version)

    domain.convert_volumes_metadata(target_version)

    domain.convert_metadata(target_version)

    try:
        domain.finalize_volumes_metadata(target_version)
    except Exception:
        log.exception("Error finalizing volume metadata")

    log.debug("Conversion of domain %s to version %s has been completed",
              domain.sdUUID, target_version)


_IMAGE_REPOSITORY_CONVERSION_TABLE = {
    ('0', '2'): v2DomainConverter,
    ('0', '3'): v3DomainConverter,
    ('2', '3'): v3DomainConverter,
    ('3', '4'): v4DomainConverter,
    ('3', '5'): v5DomainConverter,
    ('4', '5'): v5DomainConverter,
}


class FormatConverter(object):
    def __init__(self, conversionTable):
        self._convTable = conversionTable

    def _getConverter(self, sourceFormat, targetFormat):
        return self._convTable[(sourceFormat, targetFormat)]

    def convert(self, repoPath, hostId, imageRepo, isMsd, targetFormat):
        sourceFormat = imageRepo.getFormat()
        if sourceFormat == targetFormat:
            return

        converter = self._getConverter(sourceFormat, targetFormat)
        converter(repoPath, hostId, imageRepo, isMsd)


def DefaultFormatConverter():
    return FormatConverter(_IMAGE_REPOSITORY_CONVERSION_TABLE)
