from xml.etree import ElementTree
from itertools import groupby

from parallels.core.logging import get_logger
from parallels.plesk.utils.xml_rpc.plesk import messages
from parallels.core import version_tuple
from parallels.core.utils.common import partition, if_not_none, default, remove_empty_lines
from parallels.core.utils.common.xml import elem, text_elem, xml_to_string_pretty
from parallels.core.utils.common.http_xml_client import HttpXmlClient
from parallels.plesk.utils.xml_rpc.plesk.core import PleskError, Failure
from parallels.core.utils.steps_profiler import get_default_steps_profiler

profiler = get_default_steps_profiler()
logger = get_logger(__name__)


class UnsupportedOperationException(Exception):
    pass


class Client(HttpXmlClient):

    def __init__(
        self, url, username, password, secret_key, api_version, pretty_print=False, is_windows=False, host=None
    ):
        super(Client, self).__init__(url, host)
        self.url = url
        self.username = username
        self.password = password
        self.secret_key = secret_key
        self.api_version = api_version
        self.pretty_print = pretty_print
        self.is_windows = is_windows

    def send(self, operation, **request_settings):
        return self.send_many(operation, **request_settings)[0]

    def send_many(self, *operations, **request_settings):
        # check all operators are supported in the client
        unsupported_operations = [
            u"%s.%s" % (op.operator_name, op.operation_name)
            for op in operations if not self._is_operation_supported(op)
        ]
        if len(unsupported_operations) > 0:
            raise UnsupportedOperationException(
                messages.OPERATIONS_ARE_NOT_SUPPORTED_BY_CLIENT %
                (unsupported_operations, self.api_version)
            )

        # Many Plesk operations accept filter - set of identifiers to limit operation to.
        # However, when filter is empty Plesk threats it as "all objects". It is very easy
        # to get empty IDs list in filter and it is very dangerous when it is used with
        # destructive operations like 'del' or 'update'. So this API wrapper tries to fix
        # it: you need to pass special `FilterAll` filter class to really apply operation
        # to all objects.
        # Since it is not possible to call operation with empty (i.e. meaning "none") filter,
        # client must not send such operations to API. Operation which uses filter must
        # provide fake request to use instead of real one.

        # Split all operations to ones that should be skipped and ones that shouldn't.
        # But remember their original position in operation list.
        skipped, left = partition(enumerate(operations), lambda o: o[1].should_be_skipped())

        operation_responses = []
        if left:
            # Group operations by operator.
            ops = [op for (i, op) in left]
            grouped = [(n, list(g)) for n, g in groupby(ops, lambda op: op.operator_name)]
            request = self._create_packet(grouped, request_settings)

            with profiler.measure_api_call(
                self.url, 'Plesk API', "\n".join([
                    "%s/%s" % (op.operator_name, op.operation_name)
                    for op in operations
                ])
            ):
                response = self.send_xml(request)

            root = response.getroot()

            request_str = xml_to_string_pretty(request)
            response_str = remove_empty_lines(xml_to_string_pretty(response))

            # Handle system-level error
            status = root.find('system/status')
            if status is not None and status.text == 'error':
                raise PleskError(
                    code=if_not_none(root.findtext('system/errcode'), int),
                    message=root.findtext('system/errtext'),
                    url=self.url, request_str=request_str, response_str=response_str
                )

            for (operator_name, operations), operator_response in zip(grouped, root):
                assert operator_name == operator_response.tag

                # In some cases and Plesk versions, system level errors like expired license
                # are under operators in response XML. If any operator have such response - raise exception.
                if operator_response.findtext('status') == 'error':
                    raise PleskError(
                        code=if_not_none(operator_response.findtext('errcode'), int),
                        message=operator_response.findtext('errtext'),
                        url=self.url, request_str=request_str, response_str=response_str
                    )

                assert len(operations) == len(operator_response)
                for operation, operation_response in zip(operations, operator_response):
                    assert operation.operation_name == operation_response.tag

                    if operation_response.findtext('status') == 'error':
                        raise PleskError(
                            code=if_not_none(operation_response.findtext('errcode'), int),
                            message=operation_response.findtext('errtext'),
                            url=self.url, request_str=request_str, response_str=response_str
                        )

                    operation_result = operation.parse(operation_response)

                    if isinstance(operation_result, Failure):
                        operation_result.set_request_info(
                            url=self.url, request_str=request_str, response_str=response_str
                        )
                    elif isinstance(operation_result, list):
                        for item in operation_result:
                            if isinstance(item, Failure):
                                item.set_request_info(
                                    url=self.url, request_str=request_str, response_str=response_str
                                )

                    operation_responses.append(operation_result)

        fake_responses = [op.fake_response() for i, op in skipped]

        # Now merge real and fake responses matching operations order.
        responses_with_index = [
            (i, resp) for ((i, op), resp) in list(zip(left, operation_responses)) + list(zip(skipped, fake_responses))
        ]
        return [resp for i, resp in sorted(responses_with_index, key=lambda i: i[0])]

    def _create_packet(self, grouped, request_settings=None):
        request_settings = default(request_settings, {})
        request = ElementTree.ElementTree(ElementTree.Element('packet', dict(version=self.api_version)))

        if request_settings:
            request.getroot().append(elem('request-settings', [
                elem('setting', [text_elem('name', name), text_elem('value', value)])
                for name, value in request_settings.items()
            ]))

        for operator_name, operations in grouped:
            operator_node = ElementTree.Element(operator_name)
            for op in operations:
                operation_node = ElementTree.Element(op.operation_name)
                operation_node.extend(op.inner_xml())
                operator_node.append(operation_node)
            request.getroot().append(operator_node)
        return request

    def _get_http_headers(self, data, extra=None):
        headers = super(Client, self)._get_http_headers(data)
        if self.secret_key is not None:
            headers['KEY'] = self.secret_key
        elif self.username is not None and self.password is not None:
            headers['HTTP_AUTH_LOGIN'] = self.username
            headers['HTTP_AUTH_PASSWD'] = self.password
        if self.pretty_print:
            headers['HTTP_PRETTY_PRINT'] = 'true'

        return headers

    def _is_operation_supported(self, operation):
        api_version_tuple = version_tuple(self.api_version)

        return (operation.min_api_version is None or version_tuple(operation.min_api_version) <= api_version_tuple) and \
            (operation.max_api_version is None or version_tuple(operation.max_api_version) >= api_version_tuple)
