from __future__ import absolute_import
from __future__ import division

import logging
import xml.etree.ElementTree as ET

from vdsm.common import errors
from vdsm.common import response
from vdsm.virt import vmxml

from . import core
from . import hwclass


log = logging.getLogger("virt.lease")


class Error(errors.Base):
    """ Base class for lease errors """


class CannotPrepare(Error):
    msg = "Error preparing lease device {self.device}: {self.reason}"

    def __init__(self, device, reason):
        self.device = device
        self.reason = reason


class MissingArgument(Error):
    msg = "Missing required argument {self.missing} in {self.available}"

    def __init__(self, missing, available):
        self.missing = missing
        self.available = available


class Device(core.Base):
    __slots__ = ("lease_id", "sd_id", "path", "offset")

    @classmethod
    def get_identifying_attrs(cls, dev_elem):
        return {
            'devtype': core.dev_class_from_dev_elem(dev_elem),
            'name': vmxml.text(vmxml.find_first(dev_elem, 'key')),
        }

    @classmethod
    def from_xml_tree(cls, log, dev, meta):
        params = parse_xml(dev, meta)
        return cls(log, **params)

    @classmethod
    def update_device_info(cls, vm, device_conf):
        """
        We don't have anything to update yet. Keeping this interface so
        other code do not need to know that.
        """

    def __init__(self, log, **kwargs):
        for key in self.__slots__:
            if key not in kwargs:
                raise MissingArgument(key, kwargs)
        super(Device, self).__init__(log, **kwargs)

    def getXML(self):
        lease = vmxml.Element('lease')
        lease.appendChildWithArgs('key', text=self.lease_id)
        lease.appendChildWithArgs('lockspace', text=self.sd_id)
        lease.appendChildWithArgs('target', path=self.path,
                                  offset=str(self.offset))
        return lease

    def __repr__(self):
        return ("<lease.Device sd_id={self.sd_id}, "
                "lease_id={self.lease_id}, "
                "path={self.path}, "
                "offset={self.offset} "


def is_attached_to(device, xml_string):
    xpath = ("./devices/lease[key='{device.lease_id}']"
             "[lockspace='{device.sd_id}']").format(device=device)
    dom = ET.fromstring(xml_string)
    return bool(dom.findall(xpath))


def parse_xml(dev, meta):
    params = {
        'type': dev.tag,
        'device': core.find_device_type(dev),
        'lease_id': vmxml.text(vmxml.find_first(dev, 'key')),
        'sd_id': vmxml.text(vmxml.find_first(dev, 'lockspace')),
        'path': vmxml.find_attr(dev, 'target', 'path'),
        'offset': vmxml.find_attr(dev, 'target', 'offset'),
    }
    core.update_device_params(params, dev)
    core.update_device_params_from_meta(params, meta)
    return params


def find_device(vm_devices, query):
    leases = vm_devices[hwclass.LEASE][:]
    for dev in leases:
        if dev.sd_id == query["sd_id"] and dev.lease_id == query["lease_id"]:
            return dev
    raise LookupError("No such lease %s" % query)


def find_conf(vm_conf, lease):
    devices = vm_conf["devices"][:]
    for conf in devices:
        if (conf['type'] == hwclass.LEASE and
                conf['sd_id'] == lease.sd_id and
                conf['lease_id'] == lease.lease_id):
            return conf
    raise LookupError("No conf for %r" % lease)


def prepare(storage, devices):
    for device in devices:
        if _is_prepared(device):
            log.debug("Using prepared lease device: %s", device)
        else:
            _prepare_device(storage, device)
            log.debug("Prepared lease device: %s", device)


def _prepare_device(storage, device):
    lease = dict(sd_id=device["sd_id"], lease_id=device["lease_id"])
    res = storage.lease_info(lease)
    if response.is_error(res):
        raise CannotPrepare(device, res["status"]["message"])
    lease_info = res["result"]
    device["path"] = lease_info["path"]
    device["offset"] = lease_info["offset"]


def _is_prepared(device):
    return "path" in device and "offset" in device


def update_lease_element_from_info(lease_element, info, params, log):
    target = vmxml.find_first(lease_element, 'target')
    for (key, placeholder) in (
            ('path', 'LEASE-PATH'),
            ('offset', 'LEASE-OFFSET')):
        value = target.attrib.get(key, None)
        if value is None or value.startswith(placeholder):
            old_value = target.attrib[key]
            target.attrib[key] = str(info[key])
            if old_value != info[key]:
                log.info(
                    'lease %s:%s %s: %r -> %r',
                    params['sd_id'], params['lease_id'],
                    key, old_value, info[key])


def find_drive_lease_info(sd_id, lease_id, drive_objs):
    for drive_obj in drive_objs:
        volume_chain = getattr(drive_obj, "volumeChain", [])
        for vol_info in volume_chain[-1:]:
            if (vol_info['domainID'] == sd_id and
                    vol_info['volumeID'] == lease_id):
                drive_obj.log.info('Using drive lease: %s', vol_info)
                return {
                    'path': vol_info['leasePath'],
                    'offset': vol_info['leaseOffset'],
                }
    return None


def fix_parameters(vm_params):
    dev_params = vm_params.get('devices', [])
    for params in dev_params:
        if params.get('type', '') != hwclass.LEASE:
            continue
        if 'device' not in params:
            params['device'] = hwclass.LEASE
