
import logging
import xml.etree.ElementTree as et
from itertools import groupby

from parallels.common import version_tuple
from parallels.utils import partition
from parallels.utils.xml import elem, text_elem
from parallels.utils.http_xml_client import HttpXmlClient
from parallels.plesk_api.core import PleskError
from parallels.common.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(u"Operations %s are not supported by the client using protocol version %s" % 
				(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()

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

			for (operator_name, operations), operator_response in zip(grouped, root):
				assert operator_name == operator_response.tag
				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={}):
		request = et.ElementTree(et.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 = et.Element(operator_name)
			for op in operations:
				operation_node = et.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):
		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)

