from collections import namedtuple

from parallels.core import APIError
from parallels.core.utils.common import obj
from parallels.core.utils.common.xml import text_elem, seq, elem
from parallels.core.utils.entity import Entity
from parallels.plesk.utils.xml_rpc.plesk import messages

PleskErrorCode = obj(
    USER_ACCOUNT_ALREADY_EXISTS=1002,
    USER_ROLE_ALREADY_EXISTS=1007,
    OBJECT_DOES_NOT_EXIST=1013,
    OWNER_DOES_NOT_EXIST=1015,
    PASSWORD_IS_TOO_SHORT=1019,
)


class PleskError(APIError):
    """Error occurred when executing request to Plesk API"""
    def __init__(self, code, message, url=None, request_str=None, response_str=None):
        super(PleskError, self).__init__(u"Plesk error [%d]: %s" % (code, message))
        self.code = code
        self.message = message
        self._url = url
        self._request_str = request_str
        self._response_str = response_str

    @property
    def how_to_reproduce(self):
        """User-friendly description on how to reproduce API request"""
        if self._request_str is not None and self._response_str is not None and self._url is not None:
            return messages.HOW_TO_REPRODUCE_FAILED_REQUEST_WITH_DETAILS.format(
                error_message=self.message,
                request_str=self._request_str, response_str=self._response_str,
                url=self._url
            )
        else:
            return messages.HOW_TO_REPRODUCE_FAILED_REQUEST.format(error_message=self.message)


class Result(object):
    @staticmethod
    def parse(elem, parse_data=lambda e: None):
        status = elem.findtext('status')
        if status == 'ok':
            return Success(parse_data(elem))
        else:
            code = int(elem.findtext('errcode'))
            message = elem.findtext('errtext')
            return Failure(code, message)


class Success(namedtuple('Success', ('data',))):
    @property
    def ok(self):
        return True

    def check(self):
        pass


class Failure(object):
    def __init__(self, code, message):
        self._code = code
        self._message = message
        self._request_str = None
        self._response_str = None
        self._url = None

    def set_request_info(self, url, request_str, response_str):
        self._url = url
        self._response_str = response_str
        self._request_str = request_str

    @property
    def code(self):
        return self._code

    @property
    def message(self):
        return self._message

    @property
    def ok(self):
        return False

    def check(self):
        raise PleskError(self.code, self.message, self._url, self._request_str, self._response_str)
    
    @property
    def data(self):
        self.check()
        return None

    def __eq__(self, other):
        return isinstance(other, Failure) and other.code == self.code and other.message == self.message


class DescriptorProperty(Entity):
    """
    :type name: str|unicode
    :type value_type: str|unicode
    :type writable_by: set[str|unicode]
    """
    def __init__(self, name, value_type, writable_by):
        """
        :type name: str|unicode
        :type value_type: str|unicode
        :type writable_by: set[str|unicode]
        """
        self.name = name
        self.value_type = value_type
        self.writable_by = writable_by

    @staticmethod
    def parse(property_node):
        """
        :type property_node: xml.etree.ElementTree.Element
        :rtype: parallels.plesk.utils.xml_rpc.plesk.core.DescriptorProperty
        """
        return DescriptorProperty(
            name=property_node.findtext('name'),
            value_type=property_node.findtext('type'),
            writable_by={writable_by_node.text for writable_by_node in property_node.findall('writable-by')}
        )


class Descriptor(Entity):
    """
    :type properties: dict[str|unicode, DescriptorProperty]
    """
    def __init__(self, properties):
        """
        :type properties: dict[str|unicode, DescriptorProperty]
        """
        self.properties = properties

    def is_writable(self, property_name):
        """
        :type property_name: str|unicode
        :rtype: bool
        """
        if property_name not in self.properties:
            return False
        return 'none' not in self.properties[property_name].writable_by

    @staticmethod
    def parse(descriptor_node):
        """
        :type descriptor_node: xml.etree.ElementTree.Element
        :rtype: parallels.plesk.utils.xml_rpc.plesk.core.Descriptor
        """
        properties = {}
        for property_node in descriptor_node.findall('property'):
            descriptor_property = DescriptorProperty.parse(property_node)
            properties[descriptor_property.name] = descriptor_property
        return Descriptor(properties=properties)


def parse_result_set(elem, parse_function, data_indicator):
    result_nodes = elem.findall('result')

    # for each filter, when there are no objects matching filter,
    # Plesk API returns result node with status = ok, but without data
    # discard such nodes
    return [
        Result.parse(r, parse_function)
        for r in result_nodes if r.findtext('status') != 'ok' or r.find(data_indicator) is not None
    ]


def filter_owner_not_found_error(result_set):
    """Plesk error 1015 Owner not found is returned when objects not found by owner.
    Until it is fixed in Plesk, filter this error out"""
    return [
        result for result in result_set 
        if not (isinstance(result, Failure) and result.code == PleskErrorCode.OWNER_DOES_NOT_EXIST)
    ]


def filter_object_not_found_error(result_set):
    """Filter out results with "1013 owner does not exist" error

    It is not actually an error, but an indicator that no item matched filter.
    """
    return [
        result for result in result_set
        if not (isinstance(result, Failure) and result.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST)
    ]


def get_result_set_data(result_set):
    """Get list of API response data, checking whether response was successful"""
    return [
        result.data for result in result_set
    ]


def operation(name, fields):
    class Operation(namedtuple(name, fields)):
        @staticmethod
        def should_be_skipped():
            return False

    return Operation


def operation_with_filter(name, fields=tuple()):
    """Create base class for operation with filter.

    Usage:
        class Get(core.operation_with_filter('Get', ('name', 'smth'))):
            pass

    Read comments in `Client.send_many` to understand why
    operations with filter are special.
    """
    class Operation(namedtuple(name, ('filter',) + fields)):
        def should_be_skipped(self):
            return self.filter.is_empty()

        @staticmethod
        def fake_response():
            return []

        def inner_xml(self):
            if self.should_be_skipped():
                return []
            else:
                return seq(
                    elem('filter', self.filter.inner_xml()),
                    *self.data_xml()
                )

        @staticmethod
        def data_xml():
            return []

    return Operation


# FilterAll for some operators is implemented in a different way than FilterAll for other operators
# In most cases it is "<filter/>", while in some special cases it is "<filter><all/></filter>"
# See Plesk API schema to understand whether you should use FilterAll or FilterAllWithNode


class FilterAll(object):
    @staticmethod
    def inner_xml():
        return []

    @staticmethod
    def is_empty():
        return False

    def __repr__(self):
        return "FilterAll()"

    def __eq__(self, another):
        return isinstance(another, FilterAll)


class FilterAllWithNode(object):
    @staticmethod
    def inner_xml():
        return [elem('all')]

    @staticmethod
    def is_empty():
        return False


def declare_filter(name, node_name):
    """Create filter class.

    Usage:
        FilterBySiteId = declare_filter('FilterBySiteId', 'site-id')
    """
    class Filter:
        def __init__(self, ids):
            self.ids = ids

        def inner_xml(self):
            return [text_elem(node_name, rid) for rid in self.ids]

        def is_empty(self):
            return len(self.ids) == 0

        def __repr__(self):
            return "%s(%r)" % (name, self.ids)

        def __eq__(self, other):
            return type(self) == type(other) and self.ids == other.ids

    return Filter
