from parallels.plesk.utils.xml_rpc.plesk import messages
import logging
from xml.etree import ElementTree
from itertools import groupby

from parallels.core import version_tuple
from parallels.core.utils.common import partition, if_not_none, default
from parallels.core.utils.common.xml import elem, text_elem
from parallels.core.utils.common.http_xml_client import HttpXmlClient
from parallels.plesk.utils.xml_rpc.plesk.core import PleskError
from parallels.core.utils.steps_profiler import get_default_steps_profiler

profiler = get_default_steps_profiler()


class UnsupportedOperationException(Exception):
	pass


class Client(HttpXmlClient):
	logger = logging.getLogger(__name__)
	
	def __init__(self, url, username, password, api_version, pretty_print=False):
		super(Client, self).__init__(url)
		self.url = url
		self.username = username
		self.password = password
		self.api_version = api_version
		self.pretty_print = pretty_print
		
	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 (i, op): op.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()

			# 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')
				)

			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')
					)

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

		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 zip(left, operation_responses) + zip(skipped, fake_responses)
		]
		return [resp for i, resp in sorted(responses_with_index, key=lambda (i, r): i)]
	
	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.iteritems()
			]))

		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 _prepare_request_object(self, data, extra=None):
		request = super(Client, self)._prepare_request_object(data)
		request.add_header('HTTP_AUTH_LOGIN', self.username)
		request.add_header('HTTP_AUTH_PASSWD', self.password)
		if self.pretty_print:
			request.add_header('HTTP_PRETTY_PRINT', 'true')
		return request
	
	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)
