import logging
import xml.etree.ElementTree as et
import re
import ntpath

from parallels.plesk_api import operator as plesk_ops
from parallels.plesk_api.core import PleskError
from parallels.common.utils.plesk_utils import get_unix_product_root_dir
from parallels.common.utils.plesk_utils import get_windows_plesk_dir
from parallels.common.utils.plesk_utils import get_windows_dump_dir
from parallels.common.utils.plesk_utils import get_unix_dump_dir
from parallels.common.utils.plesk_restore import PleskRestoreCommand
from parallels.common.utils.windows_utils import cmd_command
from parallels.common.utils.steps_profiler import sleep

logger = logging.getLogger(__name__)

def call_plesk_restore_hosting(
		target_server, backup, backup_path, ppa_runner, domains, safe,
		disable_apsmail_provisioning=None, extra_env='', import_env='',
		target_backup_path='/tmp/plesk.backup'):
	_call_plesk_restore(
		target_server, backup, backup_path, ppa_runner, domains,
		safe, disable_apsmail_provisioning, u"hosting settings", extra_env,
		import_env, target_backup_path=target_backup_path
	)

def call_plesk_restore_aps(
		target_server, backup, backup_path, ppa_runner, domains, safe,
		disable_apsmail_provisioning=None,
		target_backup_path='/tmp/plesk.backup'):
	_call_plesk_restore(
		target_server, backup, backup_path, ppa_runner, domains, safe,
		disable_apsmail_provisioning, u"APS applications",
		"PLESK_RESTORE_SITE_APPS_ONLY=true",
		target_backup_path=target_backup_path
	)

def _call_plesk_restore(
		target_server, backup, source_backup_path, ppa_runner, domains, safe,
		disable_apsmail_provisioning, settings_description, extra_env="",
		import_env='', target_backup_path='/tmp/plesk.backup'):
	"""
	disable_apsmail_provisioning - dictionary with keys - domain names, values - if we should not provision APS mail for that domain, for example
	{'a.tld': True, 'b.tld': False} 
	- for 'a.tld' 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)
	- for 'b.tld' both web and mail hosting will be provisioned
	disable_apsmail_provisioning could be None - that means that mail service will be provisioned for every domain
	"""
	if len(domains) == 0:
		logger.debug(u"Restore %s skipped as there are no subscriptions specified", settings_description)
		return

	# STEP #1: remove backup from PPA repository if it already exists
	# If backup with such name already exists in PPA 
	# (for example, if migrator was interrupted before it cleans up backup file)
	# PMM will refuse to import it again
	# and we could get outdated backup, so remove backup file from target Plesk before import
	_remove_plesk_backup(target_server, backup)

	# STEP #2: upload backup
	logger.info(u"Upload backup dump '%s' to PPA node" % source_backup_path)
	ppa_runner.upload_file(source_backup_path, target_backup_path)

	# STEP #3: restore hosting settings from backup
	logger.info(u"Restore %s from backup", settings_description)
	if target_server.is_windows:
		_call_restore = _call_plesk_restore_windows
	else:
		_call_restore = _call_plesk_restore_unix
	_call_restore(
		backup, source_backup_path, target_server, ppa_runner, target_server.plesk_api(),
		domains, safe, disable_apsmail_provisioning, settings_description,
		extra_env, import_env, target_backup_path)

	# STEP #4: remove backup file from target Plesk, so it does not bother customers 
	# (they could see parts of the backup in their control panels if we don't remove it)
	_remove_plesk_backup(target_server, backup)

def _call_plesk_restore_windows(
	backup, source_backup_path, target_conn, runner, plesk_api, domains, safe, 
	disable_apsmail_provisioning, settings_description, 
	extra_env='', import_env='', target_backup_path='/tmp/plesk.backup'
):
	"""Restore dump on a Windows target server."""
	# Define auxiliary paths
	product_root_d = get_windows_plesk_dir(runner)
	imported_backup_dir = get_windows_dump_dir(runner)
	request_xml_filename = ntpath.join(
		product_root_d, u'panel-transfer-import-backup.xml'
	)
	pmmcli_bin = ntpath.join(product_root_d, ur"admin\bin\pmmcli.exe")

	logger.debug(u"Import backup to target panel PMM repository.")
	import_dump_request_xml = _get_pmmcli_import_dump_request( 
		source_dir=ntpath.dirname(target_backup_path), 
		source_file=ntpath.basename(target_backup_path),
		target_dir=imported_backup_dir
	)
	runner.upload_file_content(request_xml_filename, import_dump_request_xml)
	runner.sh(cmd_command(
		u'set PPA_IGNORE_FILE_MATCHING=true&&"{pmmcli}" --import-file-as-dump < "{request}"'.format(
			pmmcli=pmmcli_bin, request=request_xml_filename
		)
	))
	runner.remove_file(request_xml_filename)

	logger.debug(u"Index imported XML domain backup files.")
	# Index backup files after backup is imported: create dict with key - domain
	# name and value - backup XML file name.
	filenames = runner.sh(cmd_command(
		ur'dir "{backup_dir}\*.xml" /b /s'.format(backup_dir=imported_backup_dir)
	)).split("\n")

	backup_xml_files = {}
	for filename in filenames:
		filename = filename.strip()
		# pmmcli.exe output is broken on Windows, so we have to guess filename
		# by uploaded backup_id
		match = re.search(
			ur'domains\\([^\\]*)\\.*_info_%s.xml$' % (
				re.escape(backup.container.backup_id)
			), 
			filename
		)
		if match is not None:
			domain_name = match.group(1)
			backup_xml_files[domain_name.encode('idna')] = filename

	logger.debug(u"STEP #3: restore each domain backup XML file")
	# Only main subscription domains are required, their addon sites will be
	# restored automatically.  Restore subscriptions one by one separately, to
	# avoid them affecting others
	if extra_env != '':
		extra_env = 'set %s&&' % extra_env 

	for i, d in enumerate(domains, 1):
		logger.info(
			u"Restore %s for subscription '%s' (#%d out of %d)", 
			settings_description, d, i, len(domains)
		)
		fail_message = (
			u"Failed to restore %s of subscription "
			"with Plesk backup/restore." % (settings_description,))
		with safe.try_subscription(d, fail_message):
			if d.encode('idna') in backup_xml_files:
				dump_file = backup_xml_files[d.encode('idna')]
	
				_restore_domain(
					d, dump_file, target_conn, runner, extra_env, safe,
					settings_description)
			else:
				safe.fail_subscription(
					d, 
					u"Failed to find imported backup XML file for subscription"
				)

def _restore_domain(
		domain_name, domain_dump_file, target_conn, runner, 
		command_env, report, settings_name):
	panel_homedir = get_windows_plesk_dir(runner)
	pleskrestore_bin = ntpath.join(panel_homedir, ur"bin\pleskrestore.exe")

	# put domain names into a file to avoid unicode and IDN conversion problems
	# when calling commands on Windows and processing them by PMM
	domains_list_file = target_conn.main_node_session_file_path(
		'restore-domains-list'
	)
	runner.upload_file_content(
		domains_list_file, domain_name.encode('utf-8')
	)

	task_id = runner.sh(cmd_command(
		u'set PPA_IGNORE_FILE_MATCHING=true&&{extra_env}'
		u'"{pleskrestore}" --restore "{filename}" '
		u'-level domains -ignore-sign -filter {domains_list_file} '
		u'-async'.format(
			extra_env=command_env,
			pleskrestore=pleskrestore_bin,
			filename=domain_dump_file,
			domains_list_file=domains_list_file
		)
	))

	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
	)
	response = None

	for attempt in xrange(max_checks):
		logger.debug(
			u'Poll Plesk for restoration task status, attempt #%s', attempt
		)
		result = runner.sh(cmd_command(
			u'"{panel_homedir}/admin/bin/pmmcli" '
			u'--get-task-status {task_id}'.format(
				panel_homedir=panel_homedir,
				task_id=task_id,
			)
		))
		status_xml = et.fromstring(result.encode('utf-8'))
		status_elem = status_xml.find('data/task-status/mixed')
		if status_elem is not None and 'status' in status_elem.attrib:
			log_location = status_elem.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'Restore task finished')
			response = runner.get_file_contents(log_location)
			break

		sleep(check_interval, 'Waiting for Plesk restore task to finish')

	if 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'
		)

	if (PleskRestoreCommand.has_errors(response, domain_name)):
		report.fail_subscription(
			domain_name,
			u"An error occurred, when restoring {settings}: {text}".format(
				settings=settings_name,
				text=PleskRestoreCommand.get_error_messages(response)),
			None, is_critical=False)

def _call_plesk_restore_unix(
		backup, source_backup_path, target_conn, runner, plesk_api, domains, 
		safe, disable_apsmail_provisioning, settings_description, 
		extra_env='', import_env='', target_backup_path='/tmp/plesk.backup'):
	"""Restore migration dump on Linux target server."""
	# STEP 1: import backup to PPA repository
	logger.info(u"Import backup dump to PPA backups repository")
	dump_dir = get_unix_dump_dir(runner)
	last_slash_pos = target_backup_path.rfind('/')
	request_root_dir = target_backup_path[:last_slash_pos]
	request_file_name = target_backup_path[last_slash_pos+1:]
	import_dump_request_xml = _get_pmmcli_import_dump_request(
		source_dir=request_root_dir,
		source_file=request_file_name,
		target_dir=dump_dir
	)
	logger.debug(u"Import dump request XML: %s", import_dump_request_xml)

	panel_homedir = get_unix_product_root_dir(runner)

	stdout = runner.sh(
		u'%s %s/admin/bin/pmmcli --import-file-as-dump' % (import_env, panel_homedir,),
		stdin_content=import_dump_request_xml
	)

	import_dump_result_xml = et.fromstring(stdout)
	if import_dump_result_xml.findtext('errcode') not in set(['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: %s" % stdout)

	main_backup_xml_file_name = import_dump_result_xml.find('data/dump').attrib['name']
	match = re.search(r'(\d+)\.xml$', main_backup_xml_file_name)
	if match is None:
		raise Exception(u"Failed to import backup XML: can not find backup id. Output of pmmcli utility: %s" % stdout)
	backup_id = match.group(1)

	logger.debug(u"Imported backup id is '%s'", backup_id)

	# STEP #2: index imported XML domain backup files
	# Index backup files after backup is imported: create dict with key - domain name and value - backup XML file name.
	# 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.
	logger.debug(u"Create mapping of domain name to backup XML file name")

	domain_to_backup_file = {}
	filenames = runner.run(
		'find', [
			'-L', # follow symlinks, useful if /var/lib/psa is a symlink to a directory on another partition 
			dump_dir, 
			'-type',
			'f',
			'-name',
			u'*_%s.xml' % (backup_id,)
		]
	).splitlines()

	for filename in filenames:
		if filename != '':
			file_contents = runner.get_file_contents(filename)
			try:
				# the file_contents that we have here is in unicode() format
				# but parser (XMLParser, called from et.fromstring) needs the string in its original encoding
				# so decode it back from unicode to utf-8
				file_xml = et.fromstring(file_contents.encode('utf-8'))
				domain_node = file_xml.find('domain')
				if domain_node is not None:
					domain_to_backup_file[domain_node.attrib.get('name')] = filename
			except Exception as e:
				# Ignore all parse errors
				logger.debug(u"Exception while parsing XML (most probably could be ignored):", exc_info=e)

	# STEP #3: restore each domain backup XML file
	# Only main subscription domains are required, their addon sites will be restored automatically.
	# Restore subscriptions one by one separately, to avoid them affecting others
	for i, d in enumerate(domains, 1):
		logger.info(u"Restore %s for subscription '%s' (#%d out of %d)", settings_description, d, i, len(domains))
		with safe.try_subscription(d, u"Failed to restore %s of subscription with Plesk backup/restore." % settings_description):
			if d in domain_to_backup_file:

				if disable_apsmail_provisioning is None:
					disable_domain_apsmail = False
				elif d in disable_apsmail_provisioning:
					disable_domain_apsmail = disable_apsmail_provisioning[d]
				else:
					disable_domain_apsmail = False

				command = PleskRestoreCommand(d, runner, panel_homedir)
				succeeded = command.run(domain_to_backup_file[d], disable_domain_apsmail, extra_env)
				if not succeeded:
					safe.fail_subscription(
						d, u"An error occurred, when restoring {settings}:\n{text}".format(
							settings=settings_description,
							text=command.get_errors(),),
						None, is_critical=False)
			else:
				safe.fail_subscription(d, u"Failed to find imported backup XML file for subscription")

def _remove_plesk_backup(target_server, backup):
	logger.debug(u"Remove backup file from target Plesk")

	try:
		target_server.plesk_api().send(
				plesk_ops.backup.BackupOperator.RemoveFile(
					filename=backup.backup_info_file)).check()
	except PleskError:
		# silently skip if we failed to remove backup
		logger.debug(u'Failed to remove backup file from target Plesk, exception:', exc_info=True)

def _get_pmmcli_import_dump_request(source_dir, source_file, target_dir):
	"""Create XML that should be passed to pmmcli.exe --import-file-as-dump
	
	Return XML as a string""" 

	return """<?xml version="1.0"?>
<src-dst-files-specification guid="00000000-0000-0000-0000-000000000000" type="server">
	<src>
		<dumps-storage-credentials storage-type="file">
		<root-dir>{source_dir}</root-dir>
		<file-name>{source_file}</file-name>
		</dumps-storage-credentials>
	</src>
	<dst>
		<dumps-storage-credentials storage-type="local">
			<root-dir>{target_dir}</root-dir>
			<ignore-backup-sign>true</ignore-backup-sign>		
		</dumps-storage-credentials>
	</dst>
</src-dst-files-specification>
	""".format(
		source_dir=source_dir, source_file=source_file,
		target_dir=target_dir
	)
