import logging
import os
import textwrap
from contextlib import contextmanager
from parallels.common.utils.pmm.agent import DumpAll

import parallels.plesks_migrator
from parallels.common import run_command, MigrationError
from parallels.common.utils import windows_utils
from parallels.common.utils.pmm.windows_agent import WindowsPmmMigrationAgent
from parallels.common.utils.windows_utils import path_join as windows_path_join
from parallels.utils import safe_string_repr
from parallels.common.utils import migrator_utils

class PleskWindowsBackupTool(WindowsPmmMigrationAgent):
	"""Make migration dumps using Plesk utility 'pleskbackup'.

	This class creates migration dump on source panel and downloads it to the
	migration server. 
	"""
	logger = logging.getLogger(__name__)

	def __init__(self, source_server, migrator_server):
		self._source_server = source_server
		self._migrator_server = migrator_server

		self.errors = {
			'win_fail': textwrap.dedent("""\
				Failed to fetch information from the source server '{plesk_id}'
				({ip_address}): failed to create Plesk backup. The command
				"{command_str}" executed on the source server returned a
				non-zero exit code.
				================================================================
				Stderr:\n{stderr}
				================================================================
				Stdout:\n{stdout}
				================================================================
			"""),
		}

	def create_dump(self, filename, selection=DumpAll()):
		"""Make a full backup of Plesk server and download it."""

		@contextmanager
		def windows_winexe_backup_handle_exception():
			try:
				yield
			except run_command.WinexeRunnerException as e:
				self.logger.debug("Exception: ", exc_info=True)
				raise MigrationError(self.errors['win_fail'].format(
					plesk_id=self._source_server.node_settings.id,
					ip_address=self._source_server.node_settings.ip,
					command_str=e.command_str,
					full_command_str=e.full_command_str,
					stderr=safe_string_repr(e.stderr),
					stdout=safe_string_repr(e.stdout)
				))

		# Plesk 8 and Plesk >= 9 have different backup utility syntax.
		self.logger.info(u"Make server-wide backup")
		with self._source_server.runner() as runner:
			temp_backup_file = self._source_server.get_session_file_path('plesk.backup')
			source_plesk_dir = self._source_server.plesk_dir
			self.logger.info(u"Run Plesk backup")
			if self._source_server.plesk_major_version >= 9:
				with windows_winexe_backup_handle_exception():
					pleskbackup = ur"%s\admin\plib\cu\pleskbackup.php" % (source_plesk_dir,)
					output = runner.sh_multiple_attempts(
						ur'cmd.exe /C "{php} -dauto_prepend_file=""'
						ur' {pleskbackup} --server --configuration'
						ur' --output-file={temp_backup_file}"',
						dict(
							php=ur"%s\admin\bin\php" % (source_plesk_dir,),
							pleskbackup=pleskbackup,
							temp_backup_file=temp_backup_file
						)
					)
				if output.strip() != '':
					self.logger.warning(
						"Plesk backup output is not empty: \n%s", output
					)
			else:
				pleskbackup_path = windows_path_join(
						source_plesk_dir, ur'admin\bin\pleskbackup'
				)
				# must be the same path as in pleskbackup config file:
				backup8_path = self._source_server.get_session_file_path(
					'plesk8.backup'
				)
				config_pathname = self._upload_backuptool_config(
					backup8_path, runner
				)
				with windows_winexe_backup_handle_exception():
					runner.sh_multiple_attempts(
						ur'cmd.exe /C "{pleskbackup_path}'
						ur' --config-file={config_path}"', 
						dict(
							pleskbackup_path=pleskbackup_path,
							config_path=config_pathname
						)
					)
				runner.remove_file(config_pathname)

				self._convert_backup8(
					runner, source_plesk_dir, backup8_path,
					temp_backup_file
				)

			self.logger.info(
				ur"Download backup from '%s' on Plesk to '%s' via SMB",
				temp_backup_file, filename
			)
			runner.get_file(temp_backup_file, filename)
			self.logger.info(
				u"Removing dump file '%s' from the source server" %
				temp_backup_file
			)
			runner.remove_file(temp_backup_file)

	def _upload_backuptool_config(self, backup8_path, runner):
		"""Prepare and upload a config file for 'pleskbackup' utility.
		
		pleskbackup cannot create backup without content using command line
		options but it can create backup without content using custom
		configuration file so we upload such configuration file, run
		pleskbackup and remove this configuration file
		"""
		config_filename = 'pleskbackup_config.xml'

		config_template = migrator_utils.get_package_extras_file_path(
			parallels.plesks_migrator, config_filename
		)
		config_pathname = self._migrator_server.get_session_file_path(config_filename)
		with open(config_template, 'r') as template, open(config_pathname, 'wb') as config:
			config.write(template.read().format(plesk_backup_path=backup8_path))

		remote_config_pathname = self._source_server.get_session_file_path(
			config_filename
		)
		runner.upload_file(config_pathname, remote_config_pathname)
		return remote_config_pathname

	def _convert_backup8(
			self, runner, source_plesk_dir, backup8_path, backup_path
		):
		"""Convert Windows backup format remotely on the source server."""
		self.logger.info(u"Convert Plesk 8 backup to Plesk 9 format")
		# upload converter, convert backup to Plesk 9 format and remove converter
		runner.mkdir(ur'%s\PMM' % (source_plesk_dir,))
		converter = ur'admin/bin/pre9-backup-convert.exe'
		converter_xsl = ur'PMM/migration-dump-convert.xsl'
		for path in [converter, converter_xsl]:
			runner.upload_file(
				migrator_utils.get_package_extras_file_path(
					parallels.plesks_migrator,
					os.path.basename(path)
				),
				os.path.join(source_plesk_dir.rstrip('\\'), path)
			)

		converter_path = windows_path_join(
			source_plesk_dir, windows_utils.to_cmd(converter)
		)
		converter_xls_path = windows_path_join(
			source_plesk_dir, windows_utils.to_cmd(converter_xsl)
		)
		runner.sh(
			ur'cmd.exe /C "{converter_path} --source={backup8_path}'
			ur' --destination={backup8_path}.converted"', 
			dict(converter_path=converter_path, backup8_path=backup8_path)
		)
		for filename in [backup8_path, converter_path, converter_xls_path]:
			runner.remove_file(filename)

		# upload zip, pack converted backup, remove zip and unpacked backup
		dump_directory = ur'%s.converted' % backup8_path
		zipfile_name = ur'%s.zip"' % backup8_path
		self.create_zip(runner, dump_directory, zipfile_name)
		runner.sh(ur'cmd.exe /C "RMDIR /Q /S {directory}"', dict(directory=dump_directory))
		runner.sh(
			ur'cmd.exe /C "MOVE /Y {backup8_path}.zip {backup_path}"',
			dict(backup8_path=backup8_path, backup_path=backup_path)
		)