from parallels.core import messages
import logging
import threading
from xml.etree import ElementTree
from parallels.core.utils.common import default

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

logger = logging.getLogger(__name__)

RESTORE_TASK_POLL_INTERVAL = 1
# 2 hours
RESTORE_TASK_POLL_LIMIT = 7200

# 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, additional_env_vars=None):
		"""Import backup to PMM repository. Returns backup ID."""
		pmmcli_cmd = self._create_pmmcli()
		additional_env_vars = default(additional_env_vars, {})
		import_result = pmmcli_cmd.import_dump(target_backup_path, additional_env_vars)
		if import_result.error_code not in {'0', '116'}:
			raise Exception(messages.ERROR_FAILED_TO_IMPORT_DUMP % (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(messages.DEBUG_IMPORTED_DUMP_ID, 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,
		restore_siteapps_only=False, additional_env_vars=None
	):
		"""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()
		additional_env_vars = default(additional_env_vars, {})
		env = self._get_restore_env_variables(
			restore_siteapps_only, additional_env_vars
		)
		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)
		if restore_result.has_errors():
			for error_message in restore_result.get_error_messages(error=True):
				if not self._is_error_message_exist_in_failed_subscription(safe, domain_name, error_message):
					safe.fail_subscription(
						domain_name, messages.ERROR_WHEN_RESTORING_SETTINGS.format(
							settings=settings_description,
							text=error_message,
						),
						None, is_critical=False
					)
		for error_message in restore_result.get_error_messages(error=False):
			if not self._is_error_message_exist_in_subscription_issues(safe, domain_name, error_message):
				safe.add_issue_subscription(
					domain_name,
					Issue(
						Problem(
							'pmm_warning', Problem.WARNING,
							messages.PLESK_RESTORE_REPORT_PROBLEM
						),
						error_message
					)
				)

	@staticmethod
	def _is_error_message_exist_in_failed_subscription(safe, domain_name, error_message):
		for issue in safe.failed_objects.subscriptions[domain_name]:
			if error_message in issue.error_message:
				return True

		return False

	@staticmethod
	def _is_error_message_exist_in_subscription_issues(safe, domain_name, error_message):
		for issue in safe.issues.subscriptions[domain_name]:
			if error_message in issue.solution:
				return True

		return False

	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
		:rtype parallels.core.utils.pmm.pmmcli.PMMCLICommandBase
		"""
		raise NotImplementedError()

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

		Arguments:
		- restore_siteapps_only - if true, only APS application settings will be restored
		- additional_env_vars - dictionary with additional environment variables

		Notes on environment variables:

		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
		:rtype parallels.core.utils.pmm.pmmcli.PMMCLICommandUnix
		"""
		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, restore_siteapps_only, additional_env_vars):
		"""Get environment variables necessary for restoration

		"""
		env = dict(
			PLESK_MIGRATION_MODE=u'1',
			PLESK_RESTORE_DO_NOT_CHANGE_APACHE_RESTART_INTERVAL=u'true'
		)
		if restore_siteapps_only:
			env['PLESK_RESTORE_SITE_APPS_ONLY'] = u'true'
		env.update(additional_env_vars)

		return env


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

	def _create_pmmcli(self):
		"""Create PMMCLICommand object able to import backup, restore backup, etc
		:rtype parallels.core.utils.pmm.pmmcli.PMMCLICommandWindows
		"""
		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(
				# /b means to use "bare" format - with no headers and summary
				# /s means to search subdirectories too
				# /a-d means not to select directories, as we need files only
				ur'dir "{backup_dir}\{backup_prefix}*_info_{backup_id}.xml" /b /s /a-d'.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, restore_siteapps_only, additional_env_vars):
		"""Get environment variables necessary for restoration"""
		env = dict(
			PLESK_MIGRATION_MODE=u'1',
			PLESK_DISABLE_PROVISIONING=u'false',
		)
		if restore_siteapps_only:
			env['PLESK_RESTORE_SITE_APPS_ONLY'] = u'true'
		env.update(additional_env_vars)

		return env
