import httplib
import json
import logging
from contextlib import closing
import urlparse

from parallels.common import MigrationError
from parallels.utils import cached

logger = logging.getLogger(__name__)


class POAAPS2(object):
	"""Class for working with APS 2 in POA

	For more information check http://doc.apsstandard.org/2.1/spec/rql/
	"""

	APS_RESOURCES_URL = '/aps/2/resources/'

	def __init__(self, poa_api, subscription_id):
		"""
		:type poa_api: parallels.poa_api.Client
		"""
		self.aps = APS2Transport(poa_api, subscription_id)

	def get_resource(self, resource_type, filter_str=None):
		"""
		:param string resource_type: Resource type of aps object
		:param string filter_str:
		"""
		url = self.APS_RESOURCES_URL
		question = 'implementing(%s)' % resource_type

		if filter_str is not None:
			question = ','.join([question, filter_str])

		return self.aps.call('?'.join([url, question]))

	def post_resource(self, resource, path, data=None):
		"""
		:param string resource: resource to send request
		:param string path: method of resource who should be called
		:param dict data: request data
		:return:
		"""
		return self._request_resource('POST', resource, path, data)

	def put_resource(self, resource, path, data=None):
		"""
		:param string resource: resource to send request
		:param string path: method of resource who should be called
		:param dict data: request data
		:return:
		"""
		return self._request_resource('PUT', resource, path, data)

	def _request_resource(self, method, resource, path, data=None):
		"""
		:type method: basestring
		:type path: basestring
		:type resource: basestring
		:type data: dict
		"""
		url = u"%s%s/%s" % (self.APS_RESOURCES_URL, resource, path)
		return self.aps.call(url, method, data)


class APS2Transport(object):
	def __init__(self, poa_api, subscription_id):
		self.subscription_id = subscription_id
		self.poa = poa_api
		self._retrieve_aps_token()

	def call(self, url, method='GET', request=None, headers=None, attempt=3):
		"""
		:type url: basestring
		:type method: basestring
		:type request: dict
		:type headers: dict
		"""
		debug_message = u"Sending '{method}' to APSC: '{url}' \"{data}\"...".format(
			method=method,
			url=url,
			data=request
		)

		logger.debug(debug_message)
		request_headers = {'APS-Token': self.aps_token, 'Content-Type': 'application/json'}
		if headers is not None:
			request_headers.update(headers)

		with closing(httplib.HTTPSConnection(self.aps_url)) as conn:
			conn.request(method, url, json.dumps(request), request_headers)
			response = conn.getresponse()

			if response.status != 200:
				if self._is_expired_token(response) and attempt > 1:
					self._retrieve_aps_token()
					self.call(url, method, request, headers, attempt-1)
				else:
					raise APS2TransportError(
						u"Request to APS controller failed with status: '{status}'. Reason: '{reason}'",
						response.status, response.reason
					)

			return self._parse_response(response)

	def _retrieve_aps_token(self):
		self.aps_token, self.aps_url = get_aps_token(self.poa, self.subscription_id)

	@staticmethod
	def _parse_response(response):
		"""
		:type response: httplib.HTTPResponse
		"""
		content_type = response.getheader('content-type')
		body = response.read()
		return json.loads(body) if content_type == 'application/json' else body

	def _is_expired_token(self, response):
		if response.status != 500:
			return False

		body = self._parse_response(response)
		return isinstance(body, dict) and body.get('error') == 'APS::Hosting::Exception' and \
			body.get('message') == 'Authentication failed.'


class APS2TransportError(MigrationError):
	def __init__(self, error_message, status, reason):
		super(APS2TransportError, self).__init__(error_message.format(status=status, reason=reason))
		self.status = status
		self.reason = reason


@cached
def get_aps_token(poa_api, subscription_id):
	"""
	:type poa_api: parallels.poa_api.Client
	:return: tuple(basestring, basestring)
	"""
	aps = poa_api.get_subscription_aps_token(subscription_id)
	return aps['aps_token'], urlparse.urlparse(aps['controller_uri']).netloc
