
from __future__ import absolute_import
from __future__ import division


from contextlib import contextmanager
import logging
import operator
import threading
import xml.etree.ElementTree as ET

import libvirt
import six

from vdsm.common import conv
from vdsm.common import errors
from vdsm.common import xmlutils
from vdsm.virt import vmxml
from vdsm.virt import xmlconstants
from vdsm import utils


_CUSTOM = 'custom'
_DEVICE = 'device'

_ADDRESS = 'address'
_AUTH = 'auth'
_HOSTS = 'hosts'
_HOST_INFO = 'hostInfo'
_IO_TUNE = 'ioTune'
_NETWORK = 'network'
_PAYLOAD = 'payload'
_PORT_MIRRORING = 'portMirroring'
_REPLICA = 'diskReplicate'
_SPEC_PARAMS = 'specParams'
_VM_CUSTOM = 'vm_custom'
_VOLUME_CHAIN = 'volumeChain'
_VOLUME_CHAIN_NODE = 'volumeChainNode'
_VOLUME_INFO = 'volumeInfo'
_IGNORED_KEYS = (
    _VOLUME_INFO,
)
_DEVICE_SUBKEYS = (
    _ADDRESS, _AUTH, _CUSTOM, _HOSTS, _PAYLOAD, _PORT_MIRRORING,
    _REPLICA, _SPEC_PARAMS, _VM_CUSTOM, _VOLUME_CHAIN,
)
_NONEMPTY_KEYS = (_IO_TUNE,) + _DEVICE_SUBKEYS
_LAYERED_KEYS = {
    _HOSTS: _HOST_INFO,
    _VOLUME_CHAIN: _VOLUME_CHAIN_NODE,
}


class Error(errors.Base):
    """
    Generic metadata error
    """


class UnsupportedType(Error):
    msg = 'Unsupported {self.value} for {self.key}'

    def __init__(self, key, value):
        self.key = key
        self.value = value


class MissingDevice(Error):
    msg = 'Failed to uniquely identify one device using the given attributes'


class Metadata(object):

    def __init__(self, namespace=None, namespace_uri=None):
        """
        :param namespace: namespace to use
        :type namespace: text string
        :param namespace_uri: URI of the namespace to use
        :type namespace_uri: text string
        """
        self._namespace = namespace
        self._namespace_uri = namespace_uri
        self._prefix = None
        if namespace is not None:
            ET.register_namespace(namespace, namespace_uri)
            self._prefix = '{%s}' % self._namespace_uri

    def load(self, elem):
        values = {}
        for child in elem:
            if len(child) > 0:
                continue
            key, val = _elem_to_keyvalue(child)
            values[self._strip_ns(key)] = val
        return values

    def dump(self, element_name, **kwargs):
        elem = ET.Element(self._add_ns(element_name))
        for key, value in sorted(
            kwargs.items(),
            key=operator.itemgetter(0),
        ):
            _keyvalue_to_elem(self._add_ns(key), value, elem)
        return elem

    def make_element(self, tag, parent=None):
        if parent is None:
            return ET.Element(self._add_ns(tag))
        else:
            return ET.SubElement(parent, self._add_ns(tag))

    def find(self, elem, tag):
        return elem.find(self._add_ns(tag))

    def match(self, elem, tag):
        return elem.tag == self._add_ns(tag)

    def findall(self, elem, tag):
        for elt in elem.findall(self._add_ns(tag)):
            yield elt

    def dump_sequence(self, element_name, subelement_name, sequence):
        elem = ET.Element(self._add_ns(element_name))
        for item in sequence:
            subelem = ET.SubElement(elem, self._add_ns(subelement_name))
            subelem.text = str(item)
        return elem

    def _add_ns(self, tag):
        return (self._prefix or '') + tag

    def _strip_ns(self, tag):
        return tag.replace(self._prefix, '') if self._prefix else tag


def replace_device(dst_md, src_md, attrs):
    with dst_md.device(**attrs) as dst_dev_meta:
        with src_md.device(**attrs) as src_dev_meta:
            dst_dev_meta.clear()
            dst_dev_meta.update(src_dev_meta)


def create(name, namespace, namespace_uri, **kwargs):
    metadata_obj = Metadata(namespace, namespace_uri)
    return metadata_obj.dump(name, **kwargs)


class Descriptor(object):

    _log = logging.getLogger('virt.metadata.Descriptor')

    def __init__(
        self,
        name=xmlconstants.METADATA_VM_VDSM_ELEMENT,
        namespace=xmlconstants.METADATA_VM_VDSM_PREFIX,
        namespace_uri=xmlconstants.METADATA_VM_VDSM_URI
    ):
        self._lock = threading.Lock()
        self._name = name
        self._namespace = namespace
        self._namespace_uri = namespace_uri
        self._values = {}
        self._custom = {}
        self._devices = []

    def __bool__(self):
        with self._lock:
            return (
                bool(self._values) or bool(self._devices) or
                bool(self._custom)
            )

        return self.__bool__()

    @classmethod
    def from_xml(
        cls,
        xml_str,
        name=xmlconstants.METADATA_VM_VDSM_ELEMENT,
        namespace=xmlconstants.METADATA_VM_VDSM_PREFIX,
        namespace_uri=xmlconstants.METADATA_VM_VDSM_URI
    ):
        obj = cls(name, namespace, namespace_uri)
        obj._parse_xml(xml_str)
        return obj

    @classmethod
    def from_tree(
        cls,
        root,
        name=xmlconstants.METADATA_VM_VDSM_ELEMENT,
        namespace=xmlconstants.METADATA_VM_VDSM_PREFIX,
        namespace_uri=xmlconstants.METADATA_VM_VDSM_URI
    ):
        obj = cls(name, namespace, namespace_uri)
        obj._parse_tree(root)
        return obj

    def load(self, dom):
        md_xml = "<{tag}/>".format(tag=self._name)
        try:
            md_xml = dom.metadata(
                libvirt.VIR_DOMAIN_METADATA_ELEMENT,
                self._namespace_uri,
                0
            )
        except libvirt.libvirtError as e:
            if e.get_error_code() != libvirt.VIR_ERR_NO_DOMAIN_METADATA:
                raise

        self._log.debug(
            'loading metadata for %s: %s', dom.UUIDString(), md_xml)
        self._load(xmlutils.fromstring(md_xml))

    def dump(self, dom):
        md_xml = self._build_xml()
        dom.setMetadata(libvirt.VIR_DOMAIN_METADATA_ELEMENT,
                        md_xml,
                        self._namespace,
                        self._namespace_uri,
                        0)
        self._log.debug(
            'dumped metadata for %s: %s', dom.UUIDString(), md_xml)

    def to_xml(self):
        return self._build_xml(self._namespace, self._namespace_uri)

    def to_tree(self):
        with self._lock:
            return self._build_tree(self._namespace, self._namespace_uri)

    @contextmanager
    def device(self, **kwargs):
        dev_data = self._find_device(kwargs)
        if dev_data is None:
            dev_data = self._add_device(kwargs)
        self._log.debug('device metadata: %s', dev_data)
        data = utils.picklecopy(dev_data)
        yield data
        dev_data.clear()
        dev_data.update(utils.picklecopy(data))
        self._log.debug('device metadata updated: %s', dev_data)

    @contextmanager
    def values(self):
        with self._lock:
            data = self._values.copy()
        self._log.debug('values: %s', data)
        yield data
        with self._lock:
            self._values.clear()
            self._values.update(data)
        self._log.debug('values updated: %s', data)

    @property
    def custom(self):
        return self._custom.copy()

    def add_custom(self, values):
        self._custom.update(values)

    def all_devices(self, **kwargs):
        for data in self._matching_devices(kwargs):
            yield utils.picklecopy(data)

    def _matching_devices(self, attrs_to_match):
        for (dev_attrs, dev_data) in self._devices:
            if _match_args(attrs_to_match, dev_attrs):
                yield dev_data

    def _parse_xml(self, xml_str):
        self._parse_tree(xmlutils.fromstring(xml_str))

    def _parse_tree(self, root):
        selector = '{%s}%s' % (self._namespace_uri, self._name)
        if root.tag == 'metadata':
            md_elem = root.find('./' + selector)
        else:
            md_elem = root.find('./metadata/' + selector)
        if md_elem is not None:
            md_uuid = root.find('./uuid')
            uuid_text = '?' if md_uuid is None else md_uuid.text
            self._log.debug(
                'parsing metadata for %s: %s',
                uuid_text, xmlutils.tostring(md_elem, pretty=True))
            self._load(md_elem, self._namespace, self._namespace_uri)

    def _load(self, md_elem, namespace=None, namespace_uri=None):
        metadata_obj = Metadata(namespace, namespace_uri)
        md_data = metadata_obj.load(md_elem)
        custom_elem = metadata_obj.find(md_elem, _CUSTOM)
        with self._lock:
            if custom_elem is not None:
                self._custom = metadata_obj.load(custom_elem)
            else:
                self._custom = {}
            self._devices = [
                (dev.attrib.copy(), _load_device(metadata_obj, dev))
                for dev in metadata_obj.findall(md_elem, _DEVICE)
            ]
            md_data.pop(_CUSTOM, None)
            md_data.pop(_DEVICE, None)
            self._values = md_data

    def _build_tree(self, namespace=None, namespace_uri=None):
        metadata_obj = Metadata(namespace, namespace_uri)
        md_elem = metadata_obj.dump(self._name, **self._values)
        for (attrs, data) in self._devices:
            if data:
                dev_elem = _dump_device(metadata_obj, data)
                dev_elem.attrib.update(attrs)
                vmxml.append_child(md_elem, etree_child=dev_elem)
        if self._custom:
            custom_elem = metadata_obj.dump(_CUSTOM, **self._custom)
            vmxml.append_child(md_elem, etree_child=custom_elem)
        return md_elem

    def _build_xml(self, namespace=None, namespace_uri=None):
        with self._lock:
            md_elem = self._build_tree(namespace, namespace_uri)
            return xmlutils.tostring(md_elem, pretty=True)

    def _find_device(self, kwargs):
        devices = list(self._matching_devices(kwargs))
        if len(devices) > 1:
            raise MissingDevice()
        if not devices:
            return None
        return devices[0]

    def _add_device(self, attrs):
        data = {}
        self._devices.append((attrs.copy(), data))
        return data


def _load_device(md_obj, dev):
    info = md_obj.load(dev)

    for key in _IGNORED_KEYS:
        info.pop(key, None)

    for key in _DEVICE_SUBKEYS:
        elem = md_obj.find(dev, key)
        if elem is not None:
            if key == _PORT_MIRRORING:
                value = _load_port_mirroring(md_obj, elem)
            elif key == _REPLICA:
                value = _load_device(md_obj, elem)
            elif key in _LAYERED_KEYS:
                value = _load_layered(md_obj, elem)
            elif key == _SPEC_PARAMS:
                value = _load_device_spec_params(md_obj, elem)
            elif key == _PAYLOAD:
                value = _load_payload(md_obj, elem)
            else:
                value = md_obj.load(elem)
            info[key] = value
    return info


def _load_layered(md_obj, elem):
    return [md_obj.load(node) for node in elem]


def _dump_layered(md_obj, key, subkey, value):
    chain = md_obj.make_element(key)
    for val in value:
        vmxml.append_child(
            chain,
            etree_child=md_obj.dump(subkey, **val)
        )
    return chain


def _dump_device(md_obj, data, node_name=_DEVICE):
    elems = []
    data = utils.picklecopy(data)

    for key in _IGNORED_KEYS:
        data.pop(key, None)

    for key in _DEVICE_SUBKEYS:
        value = data.pop(key, {})
        if not value and key in _NONEMPTY_KEYS:
            continue

        if key == _PORT_MIRRORING:
            elems.append(_dump_port_mirroring(md_obj, value))
        elif key == _REPLICA:
            elems.append(_dump_device(md_obj, value, _REPLICA))
        elif key in _LAYERED_KEYS:
            elems.append(
                _dump_layered(md_obj, key, _LAYERED_KEYS[key], value)
            )
        elif key == _SPEC_PARAMS:
            elems.append(_dump_device_spec_params(md_obj, value))
        elif key == _PAYLOAD:
            elems.append(_dump_payload(md_obj, _PAYLOAD, value))
        else:
            elems.append(md_obj.dump(key, **value))

    dev_elem = md_obj.dump(node_name, **data)
    for elem in elems:
        vmxml.append_child(dev_elem, etree_child=elem)
    return dev_elem


def _load_port_mirroring(md_obj, elem):
    return [net.text for net in md_obj.findall(elem, _NETWORK)]


def _dump_port_mirroring(md_obj, value):
    return md_obj.dump_sequence(_PORT_MIRRORING, _NETWORK, value)


_VM_PAYLOAD = 'vmPayload'
_FILE_SPEC = 'file'
_PATH_SPEC = 'path'


def _load_payload(md_obj, payload_elem):
    payload = md_obj.load(payload_elem)
    payload[_FILE_SPEC] = {
        entry.attrib[_PATH_SPEC]: entry.text
        for entry in payload_elem
        if md_obj.match(entry, _FILE_SPEC)
    }
    return payload


def _load_device_spec_params(md_obj, elem):
    spec_params = md_obj.load(elem)
    payload_elem = md_obj.find(elem, _VM_PAYLOAD)
    if payload_elem is not None:
        spec_params[_VM_PAYLOAD] = _load_payload(md_obj, payload_elem)
    spec_params.pop(_IO_TUNE, None)
    return spec_params


def _dump_payload(md_obj, tag, value):
    file_spec = value.pop(_FILE_SPEC)
    payload_elem = md_obj.dump(tag, **value)

    for path, content in sorted(
        file_spec.items(),
        key=operator.itemgetter(0),
    ):
        entry = md_obj.make_element(_FILE_SPEC, parent=payload_elem)
        entry.attrib[_PATH_SPEC] = path
        entry.text = content

    return payload_elem


def _dump_device_spec_params(md_obj, value):
    value.pop(_IO_TUNE, None)
    payload = value.pop(_VM_PAYLOAD, None)
    if payload is not None:
        payload_elem = _dump_payload(md_obj, _VM_PAYLOAD, payload)
    else:
        payload_elem = None

    spec_params_elem = md_obj.dump(_SPEC_PARAMS, **value)
    if payload_elem is not None:
        vmxml.append_child(spec_params_elem, etree_child=payload_elem)

    return spec_params_elem


def _match_args(kwargs, attrs):
    for key, value in kwargs.items():
        if key not in attrs or attrs[key] != value:
            return False
    return True


def _elem_to_keyvalue(elem):
    key = elem.tag
    value = elem.text
    data_type = elem.attrib.get('type')
    if data_type is None:
        if value is None:
            value = ''
    else:
        if value is None:
            if data_type == 'str':
                value = ''
            else:
                raise ValueError(
                    'unknown type hint for %r (%s): %r' % (
                        key, elem.attrib, value))
        if data_type == 'bool':
            value = conv.tobool(value)
        elif data_type == 'int':
            value = int(value)
        elif data_type == 'float':
            value = float(value)
    return key, value


def _keyvalue_to_elem(key, value, elem):
    subelem = ET.SubElement(elem, key)
    if isinstance(value, bool):
        subelem.attrib['type'] = 'bool'
    elif isinstance(value, int):
        subelem.attrib['type'] = 'int'
    elif isinstance(value, float):
        subelem.attrib['type'] = 'float'
    elif isinstance(value, six.string_types):
        pass
    else:
        raise UnsupportedType(key, value)
    subelem.text = str(value)
    return subelem
