
from __future__ import absolute_import
from __future__ import division

import collections
import os
import threading
import xml.etree.ElementTree as ET

from vdsm.common import conv
from vdsm.common import cpuarch
from vdsm.common import errors
from vdsm.common import exception
from vdsm.config import config
from vdsm import constants
from vdsm import utils
from vdsm.virt import vmtune
from vdsm.virt import vmxml

from . import compat
from . import core
from . import drivename
from . import hwclass
from . import lease


DEFAULT_INTERFACE_FOR_ARCH = {
    cpuarch.X86_64: 'ide',
    cpuarch.PPC64: 'scsi',
    cpuarch.PPC64LE: 'scsi'
}


class DISK_TYPE:
    BLOCK = "block"
    NETWORK = "network"
    FILE = "file"


SOURCE_ATTR = {
    DISK_TYPE.FILE: 'file',
    DISK_TYPE.NETWORK: 'name',
    DISK_TYPE.BLOCK: 'dev'
}


class DRIVE_SHARED_TYPE:
    NONE = "none"
    EXCLUSIVE = "exclusive"
    SHARED = "shared"
    TRANSIENT = "transient"

    @classmethod
    def getAllValues(cls):
        return (cls.NONE, cls.EXCLUSIVE, cls.SHARED, cls.TRANSIENT)


class BLOCK_THRESHOLD:
    UNSET = "unset"
    SET = "set"
    EXCEEDED = "exceeded"


class VolumeNotFound(errors.Base):
    msg = ("Cannot find volume {self.vol_id} in drive {self.drive_name}'s "
           "volume chain")

    def __init__(self, drive_name, vol_id):
        self.drive_name = drive_name
        self.vol_id = vol_id


class InvalidBackingStoreIndex(errors.Base):
    msg = ("Backing store for path {self.path} "
           "contains invalid index {self.index!r}")

    def __init__(self, path, index):
        self.path = path
        self.index = index


VolumeChainEntry = collections.namedtuple(
    'VolumeChainEntry',
    ['uuid', 'path', 'allocation', 'index'])


BlockInfo = collections.namedtuple("BlockInfo", [
])


class Drive(core.Base):
    __slots__ = ('iface', '_path', 'readonly', 'bootOrder', 'domainID',
                 'poolID', 'imageID', 'UUID', 'volumeID', 'format',
                 'propagateErrors', 'address', 'apparentsize', 'volumeInfo',
                 'index', 'name', 'optional', 'shared', 'truesize',
                 'volumeChain', 'baseVolumeID', 'serial', 'reqsize', 'cache',
                 'extSharedState', 'drv', 'sgio', 'GUID', 'diskReplicate',
                 '_diskType', 'hosts', 'protocol', 'auth', 'discard',
                 'vm_custom', 'blockinfo', '_threshold_state', '_lock',
                 '_monitorable', 'guestName', '_iotune', 'RBD')
    VOLWM_CHUNK_SIZE = (config.getint('irs', 'volume_utilization_chunk_mb') *
                        constants.MEGAB)
    VOLWM_FREE_PCT = 100 - config.getint('irs', 'volume_utilization_percent')

    VOLWM_COW_OVERHEAD = 1.1

    @classmethod
    def update_device_info(cls, vm, device_conf):
        for x in vm.domain.get_device_elements('disk'):
            alias, devPath, name = _get_drive_identification(x)
            readonly = vmxml.find_first(x, 'readonly', None) is not None
            bootOrder = vmxml.find_attr(x, 'boot', 'order')

            devType = vmxml.attr(x, 'device')
            if devType == 'disk':
                drv = vmxml.find_attr(x, 'driver', 'type')
            else:
                drv = 'raw'
            address = vmxml.device_address(x)

            deviceDict = {'path': devPath, 'name': name,
                          'readonly': readonly, 'bootOrder': bootOrder,
                          'address': address, 'type': devType}

            def mergeDicts(deviceDef, dev):
                return dict((k, (deviceDef[k], getattr(dev, k, None)))
                            for k in deviceDef)

            vm.log.debug('Looking for drive with attributes %s', deviceDict)
            for d in device_conf:
                if (hasattr(d, 'alias') and d.alias == alias and
                        d.path != devPath):
                    vm.log.warning('updating drive %s path from %s to %s',
                                   d.alias, d.path, devPath)
                    d.path = devPath
                if d.path == devPath:
                    d.name = name
                    d.type = devType
                    d.drv = drv
                    d.alias = alias
                    d.address = address
                    d.readonly = readonly
                    if bootOrder:
                        d.bootOrder = bootOrder
                    vm.log.debug('Matched %s', mergeDicts(deviceDict, d))
            knownDev = False
            for dev in vm.conf['devices']:
                if ('alias' in dev and dev['alias'] == alias and
                        dev['path'] != devPath):
                    vm.log.warning('updating drive %s config path from %s '
                                   'to %s', dev['alias'], dev['path'],
                                   devPath)
                    dev['path'] = devPath
                if (dev['type'] == hwclass.DISK and
                        dev['path'] == devPath):
                    dev['name'] = name
                    dev['address'] = address
                    dev['alias'] = alias
                    dev['readonly'] = str(readonly)
                    if bootOrder:
                        dev['bootOrder'] = bootOrder
                    vm.log.debug('Matched %s', mergeDicts(deviceDict, dev))
                    knownDev = True
            if not knownDev:
                archIface = DEFAULT_INTERFACE_FOR_ARCH[vm.arch]
                iface = archIface if address['type'] == 'drive' else 'pci'
                diskDev = {'type': hwclass.DISK, 'device': devType,
                           'iface': iface, 'path': devPath, 'name': name,
                           'address': address, 'alias': alias,
                           'readonly': str(readonly)}
                if bootOrder:
                    diskDev['bootOrder'] = bootOrder
                vm.log.warn('Found unknown drive: %s', diskDev)
                vm.conf['devices'].append(diskDev)

    def __init__(self, log, **kwargs):
        if not kwargs.get('serial'):
            self.serial = kwargs.get('imageID'[-20:]) or ''
        self._lock = threading.Lock()
        self._path = None
        self._diskType = None
        self.device = kwargs.get('device', 'disk')
        self.propagateErrors = 'off'
        self._iotune = {}
        super(Drive, self).__init__(log, **kwargs)
        if not hasattr(self, 'vm_custom'):
            self.vm_custom = {}
        self._monitorable = True
        self._threshold_state = BLOCK_THRESHOLD.UNSET
        self.truesize = int(kwargs.get('truesize', '0'))
        self.apparentsize = int(kwargs.get('apparentsize', '0'))
        self.name = drivename.make(self.iface, self.index)
        if not hasattr(self, 'cache'):
            self._set_cache()
        self.discard = kwargs.get('discard', False)

        self.readonly = conv.tobool(
            kwargs.get('readonly', self.device == 'floppy'))

        self.blockinfo = None

        self._setExtSharedState()

    def _set_cache(self):
        self.cache = config.get('vars', 'qemu_drive_cache')

        if self.transientDisk:
            self.cache = "writethrough"
        elif self.iface == 'virtio':
            try:
                self.cache = self.vm_custom['viodiskcache']
            except KeyError:

    def _setExtSharedState(self):
        shared = str(getattr(self, "shared", "false")).lower()

        if shared == 'true':
            self.extSharedState = DRIVE_SHARED_TYPE.SHARED
        elif shared == 'false':
            if config.getboolean('irs', 'use_volume_leases'):
                self.extSharedState = DRIVE_SHARED_TYPE.EXCLUSIVE
            else:
                self.extSharedState = DRIVE_SHARED_TYPE.NONE
        elif shared in DRIVE_SHARED_TYPE.getAllValues():
            self.extSharedState = shared
        else:
            raise ValueError("Unknown shared value %s" % shared)

    @property
    def hasVolumeLeases(self):
        if self.extSharedState != DRIVE_SHARED_TYPE.EXCLUSIVE:
            return False

        for volInfo in getattr(self, "volumeChain", []):
            if "leasePath" in volInfo and "leaseOffset" in volInfo:
                return True

        return False

    def __getitem__(self, key):
        try:
            value = getattr(self, str(key))
        except AttributeError:
            raise KeyError(key)
        else:
            return value

    def __contains__(self, attr):
        return hasattr(self, attr)

    def isDiskReplicationInProgress(self):
        return hasattr(self, "diskReplicate")

    @property
    def volExtensionChunk(self):
        if self.isDiskReplicationInProgress():
            return self.VOLWM_CHUNK_SIZE * self.VOLWM_CHUNK_REPLICATE_MULT
        return self.VOLWM_CHUNK_SIZE

    @property
    def watermarkLimit(self):
        return self.VOLWM_FREE_PCT * self.volExtensionChunk // 100

    def getNextVolumeSize(self, curSize, capacity):
        nextSize = utils.round(curSize + self.volExtensionChunk,
                               constants.MEGAB)
        return min(nextSize, self.getMaxVolumeSize(capacity))

    def getMaxVolumeSize(self, capacity):
        return utils.round(capacity * self.VOLWM_COW_OVERHEAD,
                           constants.MEGAB)

    @property
    def chunked(self):
        return self.diskType == DISK_TYPE.BLOCK and self.format == "cow"

    @property
    def replicaChunked(self):
        replica = getattr(self, "diskReplicate", {})
        return (replica.get("diskType") == DISK_TYPE.BLOCK and
                replica.get("format") == "cow")

    @property
    def monitorable(self):
        with self._lock:
            return self._monitorable

    @monitorable.setter
    def monitorable(self, value):
        with self._lock:
            self._monitorable = value

    @property
    def path(self):
        with self._lock:
            return self._path

    @path.setter
    def path(self, path):
        with self._lock:
            if self._path is not None and self._path != path:
                self._threshold_state = BLOCK_THRESHOLD.UNSET
                self.log.debug(
                    "Drive %s move from %r to %r, unsetting threshold",
                    self.name, self._path, path)

            self._path = path

    @property
    def threshold_state(self):
        with self._lock:
            return self._threshold_state

    @threshold_state.setter
    def threshold_state(self, state):
        with self._lock:
            self._threshold_state = state

    def needs_monitoring(self, events_enabled):
        with self._lock:
            if self.readonly:
                return False

            if not self._monitorable:
                return False

            if not (self.chunked or self.replicaChunked):
                return False

            if events_enabled:
                return self._threshold_state in (
                    BLOCK_THRESHOLD.UNSET,
                    BLOCK_THRESHOLD.EXCEEDED)

            return True

    @property
    def diskType(self):
        return self._diskType

    @diskType.setter
    def diskType(self, value):
        if value not in SOURCE_ATTR:
            raise exception.UnsupportedOperation(
                "Unsupported diskType %r" % value)
        if self.device == 'floppy' and value != DISK_TYPE.FILE:
            raise exception.UnsupportedOperation(
                "diskType of device 'floppy' can only be 'file'")

        if self.device == 'cdrom' and value == DISK_TYPE.NETWORK:
            raise exception.UnsupportedOperation(
                "diskType of device 'cdrom' can not be 'network'")

        if self._diskType is not None and self._diskType != value:
            self.log.debug("Drive %s type changed from %r to %r",
                           self.name, self._diskType, value)

        self._diskType = value

    @property
    def transientDisk(self):
        return getattr(self, 'shared', None) == DRIVE_SHARED_TYPE.TRANSIENT

    def getLeasesXML(self):
        if not self.hasVolumeLeases:

        for volInfo in self.volumeChain[-1:]:
            device = lease.Device(self.log,
                                  lease_id=volInfo['volumeID'],
                                  sd_id=volInfo['domainID'],
                                  path=volInfo['leasePath'],
                                  offset=volInfo['leaseOffset'])
            yield device.getXML()

    @classmethod
    def get_identifying_attrs(cls, dev_elem):
        return dict(devtype=core.dev_class_from_dev_elem(dev_elem),
                    **core.get_xml_elem(dev_elem, 'name', 'target', 'dev'))

    def config(self):
        return compat.drive_config(super(Drive, self).config(), self)

    def getXML(self):
        self._validate()
        diskelem = self.createXmlElem('disk', self.diskType,
                                      ['device', 'address', 'sgio'])
        diskelem.setAttrs(snapshot='no')

        diskelem.appendChild(_getSourceXML(self))

        if self.diskType == DISK_TYPE.NETWORK and hasattr(self, 'auth'):
            diskelem.appendChild(self._getAuthXML())

        diskelem.appendChild(self._getTargetXML())

        if self.extSharedState == DRIVE_SHARED_TYPE.SHARED:
            diskelem.appendChildWithArgs('shareable')

        if self.readonly:
            diskelem.appendChildWithArgs('readonly')

        if getattr(self, 'serial', False) and self.device != 'lun':
            diskelem.appendChildWithArgs('serial', text=self.serial)

        if hasattr(self, 'bootOrder'):
            diskelem.appendChildWithArgs('boot', order=self.bootOrder)

        if self.device in ('disk', 'lun', 'cdrom'):
            diskelem.appendChild(_getDriverXML(self))

        if self.iotune:
            diskelem.appendChild(self._getIotuneXML())

        if hasattr(self, 'alias'):
            diskelem.appendChildWithArgs('alias', name=self.alias)

        return diskelem

    def get_extra_xmls(self):
        for elem in self.getLeasesXML():
            yield elem

    def getReplicaXML(self):
        disk = vmxml.Element(
            "disk",
            device=self.diskReplicate["device"],
            snapshot="no",
            type=self.diskReplicate["diskType"],
        )
        disk.appendChild(_getSourceXML(self.diskReplicate))
        disk.appendChild(_getDriverXML(self.diskReplicate))
        return disk

    def _getAuthXML(self):
        auth = vmxml.Element("auth", username=self.auth["username"])
        auth.appendChildWithArgs("secret",
                                 type=self.auth["type"],
                                 uuid=self.auth["uuid"])
        return auth

    def _getTargetXML(self):
        target = vmxml.Element('target', dev=self.name)
        if self.iface:
            target.setAttrs(bus=self.iface)
        return target

    def _getIotuneXML(self):
        iotune = vmxml.Element('iotune')
        for key, value in sorted(self.iotune.items()):
            iotune.appendChildWithArgs(key, text=str(value))
        return iotune

    def _validate(self):
        if self.diskType == DISK_TYPE.NETWORK:
            if not getattr(self, 'hosts', None):
                raise ValueError("Network disk without hosts")
            if not getattr(self, 'protocol', None):
                raise ValueError("Network disk without protocol")

        if self.device != 'lun' and hasattr(self, 'sgio'):
            raise ValueError("sgio attribute can be set only for LUN devices")

        if self.device == 'lun' and self.format == 'cow':
            raise ValueError("cow format is not supported for LUN devices")

    @property
    def _xpath(self):
        source_key = {
            DISK_TYPE.FILE: 'file',
            DISK_TYPE.BLOCK: 'dev',
            DISK_TYPE.NETWORK: 'name',
        }
        return ("./devices/disk/source[@%s='%s']" %
                (source_key[self.diskType], self.path))

    def __repr__(self):
        return ("<Drive name={self.name}, type={self.diskType}, "
                "path={self.path} threshold={self.threshold_state} "

    @property
    def iotune(self):
        return self._iotune.copy()

    @iotune.setter
    def iotune(self, value):
        iotune = value.copy()
        vmtune.validate_io_tune_params(iotune)
        self._iotune = iotune

    def volume_target_index(self, vol_id, actual_chain):
        for v in self.volumeChain:
            if v['volumeID'] == vol_id:
                return chain_index(actual_chain, vol_id, self.name)
        raise VolumeNotFound(drive_name=self.name, vol_id=vol_id)

    def volume_target(self, vol_id, actual_chain):
        index = self.volume_target_index(vol_id, actual_chain)
        if index is None:
            return None
        else:
            return "%s[%d]" % (self.name, index)

    def volume_id(self, vol_path):
        for vol in self.volumeChain:
            if self.diskType == DISK_TYPE.NETWORK:
                    if vol['path'] == vol_path:
                        return vol['volumeID']
            else:
                if os.path.realpath(vol['path']) == os.path.realpath(vol_path):
                    return vol['volumeID']
        raise LookupError("Unable to find VolumeID for path '%s'", vol_path)

    def parse_volume_chain(self, disk_xml):
        volChain = []
        index = None
        source_attr = SOURCE_ATTR[self.diskType]
        while True:
            path = vmxml.find_attr(disk_xml, 'source', source_attr)
            if not path:
                break

            if index is not None:
                try:
                    index = int(index)
                except ValueError:
                    raise InvalidBackingStoreIndex(path, index)

            alloc = None
            backingstore = next(vmxml.children(disk_xml, 'backingStore'), None)
            if backingstore is None:
                self.log.warning("<backingStore/> missing from backing "
                                 "chain for drive %s", self.name)
                break

            entry = VolumeChainEntry(self.volume_id(path), path, alloc, index)
            volChain.insert(0, entry)

            disk_xml = backingstore
            index = vmxml.attr(backingstore, 'index')
        return volChain or None

    def get_snapshot_xml(self, snap_info):
        if 'diskType' in snap_info:
            if self.diskType != snap_info['diskType']:
                raise exception.UnsupportedOperation(
                    "Unexpected diskType",
                    drive_disk_type=self.diskType,
                    snapshot_disk_type=snap_info["diskType"])

        if self.diskType == DISK_TYPE.NETWORK:
            if self.protocol != snap_info['protocol']:
                raise exception.UnsupportedOperation(
                    "Unexpected protocol",
                    drive_protocol=self.protocol,
                    snapshot_protocol=snap_info["protocol"])

        disk = vmxml.Element('disk', name=self.name, snapshot='external',
                             type=self.diskType)

        drive_info = snap_info.copy()
        drive_info["diskType"] = self.diskType
        snap_elem = _getSourceXML(drive_info)

        snap_elem.setAttrs(type=self.diskType)

        disk.appendChild(snap_elem)
        return disk

    def on_block_threshold(self, reported_path):
        with self._lock:
            if reported_path != self._path:
                self.log.debug(
                    "block threshold event mismatch drive %r path=%r "
                    "reported path=%r - ignored",
                    self.name, self._path, reported_path)
                return

            self._threshold_state = BLOCK_THRESHOLD.EXCEEDED
            self.log.info("drive %r threshold exceeded", self.name)


def chain_index(actual_chain, vol_id, drive_name):
    for entry in actual_chain:
        if entry.uuid == vol_id:
            return entry.index
    raise VolumeNotFound(drive_name=drive_name, vol_id=vol_id)


def image_id(path):
    if not os.path.basename(path):
        path = os.path.dirname(path)
    image_path = os.path.dirname(path)
    return os.path.basename(image_path)


def disable_dynamic_ownership(element):
    if element.find('seclabel') is not None:
        return
    seclabel = ET.Element('seclabel')
    seclabel.set('type', 'none')
    seclabel.set('relabel', 'no')
    seclabel.set('model', 'dac')
    element.append(seclabel)


def _getSourceXML(drive):
    needs_seclabel = False

    source = vmxml.Element('source')
    if drive["diskType"] == DISK_TYPE.BLOCK:
        needs_seclabel = True
        source.setAttrs(dev=drive["path"])
    elif drive["diskType"] == DISK_TYPE.NETWORK:
        source.setAttrs(protocol=drive["protocol"], name=drive["path"])
        for host in drive["hosts"]:
            source.appendChildWithArgs('host', **host)
    elif drive["diskType"] == DISK_TYPE.FILE:
        needs_seclabel = True
        source.setAttrs(file=drive["path"])
        if drive["device"] == 'cdrom' or drive["device"] == 'floppy':
            source.setAttrs(startupPolicy='optional')
    else:
        raise RuntimeError("Unsupported diskType %r", drive["diskType"])

    if needs_seclabel:
        disable_dynamic_ownership(source)

    return source


def _getDriverXML(drive):
    driver = vmxml.Element('driver')
    driverAttrs = {'name': 'qemu'}

    if drive['device'] != 'cdrom':
        if drive['diskType'] == DISK_TYPE.BLOCK:
            driverAttrs['io'] = 'native'
        else:
            driverAttrs['io'] = 'threads'

        if drive['format'] == 'cow':
            driverAttrs['type'] = 'qcow2'
        elif drive['format']:
            driverAttrs['type'] = 'raw'
    else:
        driverAttrs['type'] = 'raw'

    if 'discard' in drive and drive['discard']:
        driverAttrs['discard'] = 'unmap'

    try:
        driverAttrs['iothread'] = str(drive['specParams']['pinToIoThread'])
    except KeyError:
        pass

    if drive['device'] != 'cdrom':
        driverAttrs['cache'] = drive['cache']

    if (drive['propagateErrors'] == 'on' or
            conv.tobool(drive['propagateErrors'])):
        driverAttrs['error_policy'] = 'enospace'
    elif drive['propagateErrors'] == 'report':
        driverAttrs['error_policy'] = 'report'
    else:
        driverAttrs['error_policy'] = 'stop'

    driver.setAttrs(**driverAttrs)
    return driver


def _get_drive_identification(dom):
    source = vmxml.find_first(dom, 'source', None)
    if source is not None:
        devPath = (vmxml.attr(source, 'file') or
                   vmxml.attr(source, 'dev') or
                   vmxml.attr(source, 'name'))
    else:
        devPath = ''
    name = vmxml.find_attr(dom, 'target', 'dev')
    alias = core.find_device_alias(dom)
    return alias, devPath, name
