import logging
import os
import textwrap
from contextlib import contextmanager

import parallels.plesks_migrator
from parallels.common import run_command, MigrationError
from parallels.common.utils import windows_utils
from parallels.common.utils.windows_utils import path_join as windows_path_join
from parallels.utils import safe_string_repr
from parallels.common.utils.pmm_dumper import PmmDumper12
from parallels.common.utils import migrator_utils

def get_dumper(source_server, migrator_server, pmm_agent):
	"""Return a dumper object, that matches panel platform and version.
	
	Args:
		source_server:
			An instance of SourceServer, on which migration dump is to be
			created.
		migrator_server:
			An instance of MigratorServer, on which migration tools are
			executing.
		pmm_agent:
			An object of type 'PmmMigrationAgentBase', used for creating
			migration dumps on Unix
		
	Returns:
		An instance of PmmDumper
	"""
	logger = logging.getLogger(__name__)
	if not source_server.is_windows():
		logger.debug("Create a PMM-based Unix dumper; dump version will be 11.5 or later")
		return PmmDumper12(source_server, migrator_server, pmm_agent)
	else:
		logger.debug("Create a pleskbackup-based Windows dumper; dump version will be 10 or less")
		return PleskWindowsBackupTool(source_server, migrator_server)

class PleskWindowsBackupTool(object):
	"""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}
				================================================================
				1. If this happened because of a network issue, re-run the
				migration tool.
				2. Ensure that the host is up and there are no firewall rules
				that may block winexe connection to the host.
				3. To investigate the issue:
				- login to the host and run the command:
					{command_str}
				- try to run the command with winexe from the current host:
					{full_command_str}
				4. Possible cause is an inconsistency in the source Plesk
				database. PPA migration tools require 'pleskbackup' utility on
				the source server to work without any issues. If the command
				fails permanently when you try to run it on the source server, 
				please contact Parallels Plesk support to fix the inconsistency.\
			"""),
		}

	def create_dump(self, filename):
		"""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 = windows_utils.detect_plesk_dir(runner.sh)
			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
		zipfolder_vbs = 'zipfolder.vbs'
		zipfolder_vbs_path = self._source_server.get_session_file_path(
			zipfolder_vbs
		)
		runner.upload_file(
			migrator_utils.get_package_extras_file_path(
				parallels.plesks_migrator, zipfolder_vbs
			),
			zipfolder_vbs_path
		)
		
		runner.sh(
			ur'cmd.exe /C "cscript {zipfolder_vbs_path}'
			ur' {backup8_path}.converted {backup8_path}.zip"', 
			dict(zipfolder_vbs_path=zipfolder_vbs_path,
				backup8_path=backup8_path)
		)
		runner.sh(
			ur'cmd.exe /C "RMDIR /Q /S {backup8_path}.converted"',
			dict(backup8_path=backup8_path)
		)
		runner.remove_file(zipfolder_vbs_path)
		runner.sh(
			ur'cmd.exe /C "MOVE /Y {backup8_path}.zip {backup_path}"', 
			dict(backup8_path=backup8_path, backup_path=backup_path)
		)
