from parallels.common import messages
import logging
import threading
from xml.etree import ElementTree

from parallels.common.utils.migrator_utils import normalize_domain_name
from parallels.common.utils.pmm.pmmcli import PMMCLICommandUnix
from parallels.common.utils.pmm.pmmcli import PMMCLICommandWindows
from parallels.plesk_api.core import PleskError
from parallels.common.utils.pmm.restore_result import RestoreResult
from parallels.common.utils.windows_utils import cmd_command
from parallels.common.utils.steps_profiler import sleep
from parallels.common.checking import Issue
from parallels.common.checking import Problem
from parallels.utils.threading_utils import synchronized_by_lock

logger = logging.getLogger(__name__)

RESTORE_TASK_POLL_INTERVAL = 1
RESTORE_TASK_POLL_LIMIT = 7200
  # 2 hours

# There should be only one PMM restoration process running, otherwise:
# 1) PMM will explicitly fail to start the second restoration process
# if some is already running.
# 2) Plesk itself is not completely thread safe, and it is better to avoid
# running multiple operations in Plesk at the same time.
restore_hosting_settings_lock = threading.Lock()


def get_restore_hosting_utils(plesk_server):
	if plesk_server.is_windows():
		return RestoreHostingUtilsWindows(plesk_server)
	else:
		return RestoreHostingUtilsUnix(plesk_server)



class RestoreHostingUtilsBase(object):
	"""Base class for hosting restoration utility functions"""

	def __init__(self, server):
		self._server = server

	def import_backup(self, target_backup_path):
		"""Import backup to PMM repository. Returns backup ID."""
		pmmcli_cmd = self._create_pmmcli()
		import_result = pmmcli_cmd.import_dump(target_backup_path)
		if import_result.error_code not in {'0', '116'}:
			raise Exception(
				u"Failed to import backup XML: errcode must be '0' (no errors) or '116' (backup sign error). Output of pmmcli utility:\n%s" % (
					import_result.raw_stdout
				)
			)
		backup_id = import_result.backup_id
		if backup_id is None:
			raise Exception(
				messages.FAILED_IMPORT_BACKUP_XML_CAN_NOT % (
					import_result.raw_stdout
				)
			)
		logger.debug(u"Imported backup id is '%s'", backup_id)
		return import_result.backup_prefix, backup_id

	def remove_backup(self, backup_info_file):
		"""Remove backup from PMM repository. Silently ignore all errors."""
		try:
			self._create_pmmcli().delete_dump(backup_info_file)
		except PleskError:
			# silently skip if we failed to remove backup
			logger.debug(messages.FAILED_REMOVE_BACKUP_FILE_FROM_TARGET, exc_info=True)

	@synchronized_by_lock(lock=restore_hosting_settings_lock)
	def restore_hosting_settings(
		self, domain_name, domain_dump_xml, safe, settings_description,
		disable_apsmail_provisioning=False,
		restore_siteapps_only=False
	):
		"""Restore hosting settings of specified subscription
		Domain dump XML should be already in PMM repository, so use import_backup
		function before using that function.
		"""
		pmmcli_cmd = self._create_pmmcli()
		env = self._get_restore_env_variables(
			disable_apsmail_provisioning, restore_siteapps_only
		)
		restore_result = pmmcli_cmd.restore(domain_dump_xml, env)

		if restore_result.task_id is None:
			raise Exception(
				messages.RESTORATION_SUBSCRIPTION_FAILED_NO_TASK_ID % (
					restore_result.raw_stdout
				)
			)

		# Workaround for race condition in PMM:
		# when we start restoration, it starts task, and in that task it starts
		# deployer. If deployer was never running, PMM will report "failed"
		# status for restoration task to handle situation with absent/broken
		# deployer binary. So if we request task status *before*
		# deployer was started, we get "failed" task status, which is not correct.
		# The most simple way to fix - add 2 second wait to ensure that deployer
		# was started before our first task status request. Considering that
		# restoration practically never takes less than 5 seconds, that should be ok.
		sleep(2, messages.INITIALLY_WAIT_FOR_PMM_DEPLOYER_START)

		logger.debug(
			messages.WAITING_FOR_RESTORE_TASK_FINISH_CHECK,
			RESTORE_TASK_POLL_INTERVAL, RESTORE_TASK_POLL_LIMIT
		)

		status = None

		for attempt in xrange(RESTORE_TASK_POLL_LIMIT):
			logger.debug(
				messages.POLL_PLESK_FOR_RESTORATION_TASK_STATUS, attempt
			)
			status = pmmcli_cmd.get_task_status(restore_result.task_id)
			if status.is_running:
				sleep(RESTORE_TASK_POLL_INTERVAL, messages.WAITING_FOR_PLESK_RESTORE_TASK_FINISH)
			else:
				break

		assert status is not None  # loop executed at least once

		if status.is_running:
			# if still running - raise exception
			raise Exception(
				messages.RESTORATION_SUBSCRIPTION_FAILED_TIMED_OUT_WAITING)

		if status.is_interrupted:
			# if interrupted by /usr/local/psa/admin/bin/pmmcli --stop-task
			raise Exception(
				messages.RESTORATION_SUBSCRIPTION_FAILED_RESTORATION_TASK_STOPPED)


		result_log = self._download_result_xml(status.log_location)
		restore_result = RestoreResult(domain_name, result_log)
		succeeded = not restore_result.has_errors()
		if not succeeded:
			for error_message in restore_result.get_error_messages(error=True):
				safe.fail_subscription(
					domain_name, u"An error occurred, when restoring {settings}:\n{text}".format(
						settings=settings_description,
						text=error_message,
					),
					None, is_critical=False
				)
		for error_message in restore_result.get_error_messages(error=False):
			safe.add_issue_subscription(
				domain_name,
				Issue(
					Problem(
						'pmm_warning', Problem.WARNING,
						u"Plesk restore report problem:"
					),
					error_message
				)
			)

	def index_imported_domain_backup_files(self, backup_prefix, backup_id):
		"""Create mapping {domain: backup file path} for backup imported to repository

		We find all XML files with corresponding backup_id and then check and
		if it is a domain backup XML, then we parse it and take domain name from <domain> node
		ATM it is impossible to reliably get file name where domain information is
		stored by domain name in case of long domain names, so we index files in such way.
		"""
		raise NotImplementedError()

	def _create_pmmcli(self):
		"""Create PMMCLICommand object able to import backup, restore backup, etc"""
		raise NotImplementedError()

	def _get_restore_env_variables(self, disable_apsmail_provisioning, restore_siteapps_only):
		"""Get environment variables necessary for restoration as dictionary

		Arguments:
		- disable_aps_mail_provisioning - if true, that means that web hosting will be provisioned,
		but APS mail won't (mail settings will appear in the hosting panel,
		but nothing will happen on mail server; that is necessary for SmarterMail assimilation scenario)
		- restore_siteapps_only - if true, only APS application settings will be restored

		Notes on environment variables:

		PLESK_DISABLE_APSMAIL_PROVISIONING must stay defined, as it indicates to
		PMM 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 PMM it
		leaves Apache restart interval equal to 999999, and there is 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)
		"""
		raise NotImplementedError()

	def _get_backup_xml_domain(self, filename):
		with self._server.runner() as runner:
			file_contents = runner.get_file_contents(filename)
		try:
			# the file_contents that we have here is in unicode() format
			# but parser (XMLParser, called from ElementTree.fromstring) needs the string in its original encoding
			# so decode it back from unicode to utf-8
			file_xml = ElementTree.fromstring(file_contents.encode('utf-8'))
			domain_node = file_xml.find('domain')
			if domain_node is not None:
				domain_name = normalize_domain_name(
					domain_node.attrib.get('name')
				)
				return domain_name
		except Exception:
			# Ignore all parse errors
			logger.debug(messages.EXCEPTION_WHILE_PARSING_XML_MOST_PROBABLY, exc_info=True)
			return None

	def _download_result_xml(self, log_location):
		"""Download restoration result XML contents."""
		if log_location is None:
			raise Exception(
				messages.RESTORATION_SUBSCRIPTION_FAILED_NO_RESTORATION_STATUS)
		logger.debug(messages.RETRIEVING_RESTORATION_LOG_S, log_location)
		with self._server.runner() as runner:
			return runner.get_file_contents(log_location)


class RestoreHostingUtilsUnix(RestoreHostingUtilsBase):
	"""Unix-specific hosting restoration functions"""

	def _create_pmmcli(self):
		"""Create PMMCLICommand object able to import backup, restore backup, etc"""
		return PMMCLICommandUnix(self._server)

	def index_imported_domain_backup_files(self, backup_prefix, backup_id):
		"""Create mapping {domain: backup file path} for backup imported to repository

		See comment in base class for details.
		"""
		domain_to_backup_file = {}
		with self._server.runner() as runner:
			file_names = runner.run(
				'/usr/bin/find', [
					'-L',  # follow symlinks, useful if /var/lib/psa is a symlink to a directory on another partition
					self._server.dump_dir,
					'-type',
					'f',
					'-name',
					u'%s*_%s.xml' % (backup_prefix, backup_id,)
				]
			).splitlines()
		for filename in file_names:
			if filename != '':
				domain_name = self._get_backup_xml_domain(filename)
				if domain_name is not None:
					domain_to_backup_file[domain_name] = filename

		return domain_to_backup_file

	def _get_restore_env_variables(self, disable_apsmail_provisioning, restore_siteapps_only):
		"""Get environment variables necessary for restoration

		"""
		env = dict(
			PLESK_MIGRATION_MODE=u'1',
			PLESK_DISABLE_PROVISIONING=u'false',
			PLESK_DISABLE_APSMAIL_PROVISIONING=(u'true' if disable_apsmail_provisioning else u'false'),
			PLESK_RESTORE_DO_NOT_CHANGE_APACHE_RESTART_INTERVAL=u'true'
		)
		if restore_siteapps_only:
			env['PLESK_RESTORE_SITE_APPS_ONLY'] = u'true'

		return env


class RestoreHostingUtilsWindows(RestoreHostingUtilsBase):
	"""Windows-specific hosting restoration commands"""

	def _create_pmmcli(self):
		"""Create PMMCLICommand object able to import backup, restore backup, etc"""
		return PMMCLICommandWindows(self._server)

	def index_imported_domain_backup_files(self, backup_prefix, backup_id):
		"""Create mapping {domain: backup file path} for backup imported to repository

		See comment in base class for details.
		"""
		with self._server.runner() as runner:
			file_names = runner.sh(cmd_command(
				ur'dir "{backup_dir}\{backup_prefix}*_info_{backup_id}.xml" /b /s'.format(
					backup_dir=self._server.dump_dir,
					backup_prefix=backup_prefix,
					backup_id=backup_id
				)
			)).split("\n")
		backup_xml_files = {}
		for filename in file_names:
			filename = filename.strip()
			if filename != '':
				domain_name = self._get_backup_xml_domain(filename)
				if domain_name is not None:
					backup_xml_files[domain_name] = filename
		return backup_xml_files

	def _get_restore_env_variables(self, disable_apsmail_provisioning, restore_siteapps_only):
		"""Get environment variables necessary for restoration"""
		if disable_apsmail_provisioning:
			raise NotImplementedError(
				messages.DISABLE_APS_MAIL_PROVISIONING_FLAG_IS)

		env = dict(
			PLESK_MIGRATION_MODE=u'1',
			PLESK_DISABLE_PROVISIONING=u'false',
		)
		if restore_siteapps_only:
			env['PLESK_RESTORE_SITE_APPS_ONLY'] = u'true'

		return env
