import logging
import xml.etree.ElementTree as et
from parallels.common.utils.steps_profiler import sleep

logger = logging.getLogger(__name__)

class PleskRestoreCommand(object):
	"""Abstraction of 'pleskrestore' CLI utility.

	Notes on 'pleskrestore' environment variables:

	PLESK_DISABLE_APSMAIL_PROVISIONING must stay defined, as it indicates to
	pleskrestore that it should work despite heterogeneous mode

	PLESK_DISABLE_PROVISIONING=false must stay defined for the same reasons
	when migrating to PPA 11.5 which does not have latest micro updates
	installed, and does not know about PLESK_DISABLE_APSMAIL_PROVISIONING (this
	could be cleaned up on the next major update when migrator requires new
	version of PPA)

	PLESK_RESTORE_DO_NOT_CHANGE_APACHE_RESTART_INTERVAL is set to 'true' as we
	control Apache restart interval ourselves. Reasons:
	
	- we need to set it once at the beginning of restore hosting step and
	  revert it back at the end of the step, no need to pull it for each
	  subscription
	- in case of critical failure (which sometimes happens) of pleskrestore it
	  leaves Apache restart inverval equal to 999999, and Plesk team has no
	  idea how to fix it now in a reasonable time
	- we need to leave an ability to customize the interval to customer
	
	PLESK_MIGRATION_MODE is used to specify PMM that database assimilation
	scenario is possible, and if database with such name already exists on the
	server PMM will try to just register it in Plesk (security is considered
	too - check PMM code for more details)
	"""
	def __init__(self, domain, runner, panel_homedir):
		"""Object constructor.

		Args:
			domain: Domain to be restored
			runner: SSH connection object for Plesk server
			panel_homedir: Directory, where Plesk is installed
		"""
		self.runner = runner
		self.domain = domain
		self.panel_homedir = panel_homedir

	def run(self, domain_dumpxml, disable_domain_apsmail, env):
		"""Run 'pleskrestore' command.

		Args:
			domain_dumpxml: migration dump XML for the domain
			disable_domain_apsmail: a 'pleskrestore' option we need to pass
				separately for some reason
			env: additional environment variables to be set
		
		Returns:
			'True', if command succeeded; 'False' in case, if an error has occurred.
		"""
		task_id = self.runner.sh(
			u'PLESK_MIGRATION_MODE=1 PLESK_DISABLE_PROVISIONING=false'
			u' PLESK_DISABLE_APSMAIL_PROVISIONING={plesk_disable_apsmail_provisioning}'
			u' PLESK_RESTORE_DO_NOT_CHANGE_APACHE_RESTART_INTERVAL=true %s'
			u' {panel_homedir}/bin/pleskrestore --restore {domain_backup_xml}'
			u' -ignore-sign -verbose -level domains -async' % (env,),
			dict(
				plesk_disable_apsmail_provisioning='true' if disable_domain_apsmail else 'false',
				panel_homedir=self.panel_homedir,
				domain_backup_xml=domain_dumpxml
			)
		)

		check_interval = 5
		max_checks = 360 

		logger.debug(
			u'Waiting for restore task to finish, '
			u'check interval: %s seconds, '
			u'maximum check attempts %s', 
			check_interval, max_checks
		)

		for attempt in xrange(max_checks):
			logger.debug(
				u'Poll Plesk for restoration task status, attempt #%s', attempt
			)
			status = self.get_status(task_id)
			if self.is_status_running(status):
				sleep(check_interval, 'Waiting for Plesk restore task to finish')
			else:
				break

		self.response = self.get_result_log(task_id)
		if self.response is None:
			raise Exception(
				u'Restoration of subscription failed: timed out '
				u'waiting for the restore task. '
				u'Check debug.log and PMM restoration logs for more details'
			)

		return not self._has_errors()

	def get_status(self, task_id):
		"""Request 'pleskrestore' process status.
		
		Args:
		task_id: ID of a started pleskrestore process.
		
		Returns:
			XML node that contains status.
		"""
		result = self.runner.sh(
			u'{panel_homedir}/admin/bin/pmmcli '
			u'--get-task-status {task_id}',
			dict(panel_homedir=self.panel_homedir, task_id=task_id)
		)
		status_xml = et.fromstring(result.encode('utf-8'))
		return status_xml.find('data/task-status/mixed')

	def is_status_running(self, status_elem):
		"""Check, whether 'pleskrestore' process is running or finished."""
		if status_elem is not None and 'status' in status_elem.attrib:
			# 'status' node is '<mixed status="success" ...>'
			logger.debug(u'Restore task finished')
			return False
		else:
			# 'status' node is '<mixed>'
			logger.debug(u'Restore task is running')
			return True

	def get_result_log(self, task_id):
		"""Get pleskrestore log contents."""
		status = self.get_status(task_id)
		if not self.is_status_running(status):
			log_location = status.attrib.get('log-location')
			if log_location is None:
				raise Exception(
					u'Restoration of subscription failed: no restoration '
					u'status log is available. '
					u'Check debug.log and PMM restoration logs for more'
					u'details'
				)
			logger.debug(u'Retrieving the pleskrestore log "%s"', log_location)
			return self.runner.get_file_contents(log_location)
		else:
			return None

	def _has_errors(self):
		return self.has_errors(self.response, self.domain)

	def get_errors(self):
		return self.get_error_messages(self.response, self.domain)

	@staticmethod
	def parse_response(stdout):
		"""Get XML out of Plesk restore stdout.

		Returns:
			XML object, if response contains one. Otherwise return None.
		"""
		start_xml_declaration = '<?xml version="1.0" encoding="UTF-8"?>'
		start_xml_declaration_pos = stdout.find(start_xml_declaration)
		end_tag = "</restore>"
		end_tag_pos = stdout.find(end_tag)
		if start_xml_declaration_pos != -1 and end_tag_pos != -1:
			xml_str_content = stdout[
					start_xml_declaration_pos:end_tag_pos+len(end_tag)].strip()
			try:
				return et.fromstring(xml_str_content)
			except Exception as e:
				logger.debug(u"Error while parsing Plesk restore XML", exc_info=e)
				return None
		else:
			return None

	@staticmethod
	def has_errors(response, domain_name):
		"""Search for error messages in response, ignore some known errors.
		
		Args:
			response: stdout of 'pleskrestore' command
			domain_name: the name of the domain specified in arguments for
			'pleskrestore'
		Returns:
			'True', if errors are found, 'False' otherwise.
		"""
		xml_response = PleskRestoreCommand.parse_response(response)
		ignored_codes = {
			# code for regenerated password for sys, main, db users with empty password 
			'PasswordGenerated'
		}
		ignored_descriptions = {
			# ignore error about missing PPA functionality
			"Execution of /usr/local/psa/admin/plib/api-cli/domain_pref.php"
			u" --update {domain} -webmail smwebmail -ignore-nonexistent-options"
			u" failed with return code 1."
			"""
	Stderr is
	An error occurred while updating domain settings: Cannot set webmail: Webmail service is switched off.

	""".format(domain=domain_name)
		}

		if xml_response is None:
			# no output -> no errors
			return False

		for message_object in xml_response.findall('.//message'):
			if message_object.attrib.get('code') in ignored_codes:
				continue
			elif message_object.find('description').text in ignored_descriptions:
				continue
			elif message_object.attrib.get('code') == 'resolver' \
					and message_object.attrib.get('id') == 'component_not_installed_spam_assassin':
				# spamassassin is not available now, but was available before -
				# we do not want to spam customer with such errors
				continue
			else:
				return True	
		return False

	@staticmethod
	def get_error_messages(response, domain=''):
		"""Extract error messages from raw output of 'pleskrestore'.

		Enhance message wording, when it is too cryptic or too verbose.
		"""
		replacements = {
			"It will be disabled for the following objects: %s" % (domain,):
				"This feature will be disabled.",
			'Backup file has wrong signature for this server': '',
			'managed_runtime_version': '.NET runtime'
		}

		# find messages in XML
		xml_response = PleskRestoreCommand.parse_response(response)
		messages_raw = []
		if xml_response is not None:
			for message_node in xml_response.findall('.//message'):
				messages_raw.append(
					message_node.find('description').text.strip()
				)

		# perform replacements
		messages = []
		for message in messages_raw:
			for replace_me, new_text in replacements.iteritems():
				message = message.replace(replace_me, new_text)
			messages.append(message)

		return "\n\n".join(messages)
