from parallels.core import messages
import os
import ntpath

import logging

from xml.etree import ElementTree

from parallels.core.utils import download
from parallels.core import MigrationNoContextError
from parallels.core.utils import get_agents_base_path
from parallels.core.utils.yaml_utils import read_yaml, write_yaml
from parallels.core.utils.pmm.agent import PmmMigrationAgentBase, DumpAll, DumpSelected
from parallels.core.utils import windows_utils
from parallels.core.utils.windows_utils import path_join as windows_path_join, get_from_registry, find_in_registry
from parallels.core.utils.common import safe_string_repr
from parallels.core.run_command import BaseRunnerException

from parallels.core.utils import migrator_utils
import parallels.plesk.source.plesk

logger = logging.getLogger(__name__)


class WindowsPmmMigrationAgent(PmmMigrationAgentBase):
	"""Abstract class for Windows migration agents (Plesk, Helm, Expand)."""

	def _run(self, local_dump_path, local_log_filename, selection=DumpAll()):
		"""Create migration dump and download it to session directory."""
		logger.info(messages.UTILS_PMM_WINDOWS_AGENT_CREATE_MIGRATION_DUMP.format(dump_path=local_dump_path))
		self.local_log_filename = local_log_filename
		with self._source_server.runner() as runner:
			dump_path = self._make_dump(runner, selection)
			self._download_dump(dump_path, local_dump_path, runner)

	def _run_shallow(self, local_dump_path, local_log_filename):
		"""Create a dump with simplified structure, useful for creation of migration list."""
		logger.info(messages.UTILS_PMM_WINDOWS_AGENT_CREATE_SHALLOW_DUMP.format(dump_path=local_dump_path))
		self.local_log_filename = local_log_filename
		with self._source_server.runner() as runner:
			file_path = self._make_shallow_dump(runner)
			runner.get_file(file_path, local_dump_path)

	def _run_capability(self, local_dump_path, local_log_filename, selection=DumpAll()):
		"""Create capability dump and download it to session directory."""
		logger.info(messages.UTILS_PMM_WINDOWS_AGENT_CREATE_CAPABILITY_DUMP.format(dump_path=local_dump_path))
		self.local_log_filename = local_log_filename
		with self._source_server.runner() as runner:
			file_path = self._make_capability_dump(runner, selection)
			runner.get_file(file_path, local_dump_path)

	def _make_dump(self, runner, selection):
		raise NotImplementedError()

	def _make_shallow_dump(self, runner):
		raise NotImplementedError()

	def _make_capability_dump(self, runner, selection):
		raise NotImplementedError()

	def _create_filter_file(self, runner, selection):
		migration_objects_list = ElementTree.Element("migration-objects-list")

		resellers = ElementTree.SubElement(migration_objects_list, "resellers")
		for reseller in selection.resellers:
			ElementTree.SubElement(resellers, "reseller").text = reseller
		clients = ElementTree.SubElement(migration_objects_list, "clients")
		for client in selection.clients:
			ElementTree.SubElement(clients, "client").text = client
		domains = ElementTree.SubElement(migration_objects_list, "domains")
		for domain in selection.domains:
			ElementTree.SubElement(domains, "domain").text = domain

		filter_filename = self._source_server.get_session_file_path("filter.xml")
		runner.upload_file_content(
			filter_filename,
			ElementTree.tostring(migration_objects_list, 'utf-8', 'xml')
		)
		return filter_filename

	def _get_create_dump_command(self, runner, selection=DumpAll()):
		if isinstance(selection, DumpSelected):
			options = {
				'agent_script': self._get_agent_path(),
				'dump_command': '--create-full-migration-dump',
				'filter_file': self._create_filter_file(runner, selection)
			}
			command = ur'cmd.exe /C "{agent_script} {dump_command} {filter_file}"'
		else:
			options = {
				'agent_script': self._get_agent_path(),
				'dump_command': '--create-full-migration-dump',
			}
			command = ur'cmd.exe /C "{agent_script} {dump_command}"'
		return command, options

	def _get_create_shallow_dump_command(self):
		options = {
			'agent_script': self._get_agent_path(),
			'dump_command': '--create-shallow-migration-dump',
		}
		command = ur'cmd.exe /C "{agent_script} {dump_command}"'
		return command, options

	def _get_create_capability_dump_command(self, runner, selection):
		if isinstance(selection, DumpSelected):
			options = {
				'agent_script': self._get_agent_path(),
				'dump_command': '--create-capability-info-dump',
				'filter_file': self._create_filter_file(runner, selection)
			}
			command = ur'cmd.exe /C "{agent_script} {dump_command} {filter_file}"'
		else:
			options = {
				'agent_script': self._get_agent_path(),
				'dump_command': '--create-capability-info-dump',
			}
			command = ur'cmd.exe /C "{agent_script} {dump_command}"'
		return command, options

	@staticmethod
	def _get_remote_dump_path(agent_stdout):
		"""Parse agent stdout, return the directory, where migration dump is stored."""
		return agent_stdout.strip('\r\n"')

	@staticmethod
	def _download_dump(remote_dump_path, local_dump_path, runner):
		"""Download migration dump files as a ZIP archive with specified name.

		:type runner: parallels.core.run_command.BaseRunner
		"""
		logger.info(messages.DOWNLOADING_MIGRATION_DUMP_S % (remote_dump_path,))
		is_zipfile = remote_dump_path[-4:] == '.zip'
		if is_zipfile:
			zipfilename = remote_dump_path
			runner.get_file(zipfilename, local_dump_path)
		else:
			runner.download_directory_as_zip(remote_dump_path, local_dump_path)

	def _get_agent_path(self):
		return ntpath.join(self._get_base_path(), 'WINAgentMng.exe')

	def _deploy(self):
		if self._get_base_path() is None:
			with self._source_server.runner() as runner:
				storage_path = self._get_installer_path_in_storage()
				installer_filename = self._get_installer_filename()

				if os.path.exists(storage_path) is False:
					# download installer and save it into storage
					storage_url = 'http://autoinstall.plesk.com/panel-migrator/agents/%s' % installer_filename
					logger.info(messages.DOWNLOAD_DUMP_AGENT.format(url=storage_url))
					download(storage_url, storage_path)
				# upload installer
				installer_path = self._source_server.get_session_file_path(installer_filename)
				logger.info(messages.UPLOAD_DUMP_AGENT.format(
					path=installer_path,
					server=self._source_server.description())
				)
				runner.upload_file(storage_path, installer_path)

				self._check_requirements(runner)

				possible_registry_keys = [
					'HKLM\SOFTWARE\Wow6432Node\PanelMigrator\Agent',
					'HKLM\SOFTWARE\PanelMigrator\Agent'
				]

				# check if agent was already installed and remove it
				base_path = get_from_registry(runner, possible_registry_keys, 'INSTALLDIR')
				if base_path is not None:
					runner.sh_unchecked('msiexec /x "%s" /qn' % installer_path)

				# install agent and store path in session
				logger.info(messages.INSTALL_DUMP_AGENT.format(server=self._source_server.description()))

				cmd = 'msiexec /i "%s" /qn' % installer_path
				exit_code, stdout, stderr = runner.sh_unchecked(cmd)
				if exit_code != 0:
					if exit_code == 1618:
						raise BaseRunnerException(
							messages.UNABLE_TO_INSTALL_DUMP_AGENT_ANOTHER_INSTALLER % (
								self._source_server.description()
							)
						)
					else:
						raise BaseRunnerException(
							messages.INSTALL_COMMAND_FAILED_WITH_EXIT_CODE %
							(
								cmd,
								exit_code,
								safe_string_repr(stdout.replace('\x00', '')),
								safe_string_repr(stderr)
							)
						)

				base_path = get_from_registry(runner, possible_registry_keys, 'INSTALLDIR')
				if base_path is not None:
					write_yaml(
						self.global_context.session_files.get_path_to_migration_agent(self._source_server.ip()),
						base_path
					)
				return base_path

	def _check_requirements(self, runner):
		"""
		Check that source server meet requrements to deplay and run Panel Migrator Agent,
		raise exception otherwise
		:type runner: parallels.core.run_command.BaseRunner
		"""
		def _raise_exeption():
			raise MigrationNoContextError(
				messages.UNABLE_USE_PANEL_MIGRATOR_AGENT_SERVER.format(
					server=self._source_server.description()
				)
			)
		if not find_in_registry(runner, ['HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework'], 'v4.*'):
			_raise_exeption()

	def _get_base_path(self):
		return read_yaml(
			self.global_context.session_files.get_path_to_migration_agent(self._source_server.ip()),
			True
		)

	def _get_installer_path_in_storage(self):
		return os.path.join(get_agents_base_path(), self._get_installer_filename())

	def _get_installer_filename(self):
		target_plesk_version = self.global_context.conn.target.plesk_server.get_plesk_version()
		if target_plesk_version < (12, 5):
			agent_version = '12.0'
		else:
			agent_version = '12.5'

		return 'panel-migrator-agent-%s.msi' % agent_version

	def uninstall(self):
		"""Windows agent uninstallation is not supported."""
		pass


class WindowsPleskPmmMigrationAgent(WindowsPmmMigrationAgent):
	"""Plesk migration agent for Windows."""

	def _make_dump(self, runner, selection=DumpAll()):
		"""Run agent on source server, generate migration dump."""
		command, options = self._get_create_dump_command(runner, selection)
		result = runner.sh(command, options)
		dump_path = self._get_remote_dump_path(result)
		if not self._source_server.plesk_major_version >= 9:
			dump_path = self._convert_dump_to_plesk9(dump_path, runner)
		return dump_path

	def _make_shallow_dump(self, runner):
		"""Run agent on source server, generate shallow migration dump."""
		command, options = self._get_create_shallow_dump_command()
		result = runner.sh(command, options)
		return self._get_remote_dump_path(result)

	def _make_capability_dump(self, runner, selection):
		"""Run agent on source server, generate capability dump."""
		command, options = self._get_create_capability_dump_command(runner, selection)
		result = runner.sh(command, options)
		return self._get_remote_dump_path(result)

	def _convert_dump_to_plesk9(self, pp8_dump_path, runner):
		"""Convert Windows backup format remotely (on the source server)."""
		converter_path = self.upload_plesk9_converter(runner)
		logger.info(messages.CONVERT_PLESK_DUMP_PLESK_FORMAT)

		# Workaround for issue PMT-436:
		# make path to backup file as short as possible, but root cause, as we suppose,
		# should be fixed in plesk 8 backup converter
		source_dump_path = self._source_server.get_session_file_path('s')
		runner.sh(
			ur'cmd.exe /C "move /Y {pp8_dump_path} {source_dump_path}',
			dict(
				pp8_dump_path=pp8_dump_path,
				source_dump_path=source_dump_path
			)
		)

		converted_dump_path = self._source_server.get_session_file_path('c')
		runner.sh(
			ur'cmd.exe /C "IF EXIST {tmpdir} (rmdir /Q /S {tmpdir})"',
			dict(tmpdir=converted_dump_path))
		runner.sh(
			u'cmd.exe /C "{converter_path} --source={source_dump_path} --destination={converted_dump_path}"',
			dict(
				converter_path=converter_path,
				source_dump_path=source_dump_path,
				converted_dump_path=converted_dump_path
			)
		)
		return converted_dump_path

	def upload_plesk9_converter(self, runner):
		"""Upload Plesk dump format converter to the source server."""
		source_plesk_dir = self._source_server.plesk_dir
		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]:
			local_path = migrator_utils.get_package_extras_file_path(
				parallels.plesk.source.plesk,
				os.path.basename(path)
			)
			remote_path = os.path.join(source_plesk_dir.rstrip('\\'), path)
			runner.upload_file(local_path, remote_path)

		upload_path = windows_path_join(source_plesk_dir, windows_utils.to_cmd(converter))
		logger.debug(messages.DEBUG_UPLOAD_CONVERTER, upload_path)
		return upload_path
