from parallels.core import messages, MigrationNoRepeatError
import logging
import ntpath

from parallels.core.actions.base.subscription_action import SubscriptionAction
from parallels.core.actions.utils.multithreading_properties import MultithreadingProperties
from parallels.core.utils import subscription_filter
from parallels.core.utils import windows_utils
from parallels.core.utils.hosting_analyser_utils import apply_hosting_analyser_strategy
from parallels.core import MigrationError
from parallels.core import migrator_config
from parallels.core.utils import config_utils
from parallels.core.utils.paths import web_paths
from parallels.core.utils.paths.copy_web_content import BaseWebPathConverter

logger = logging.getLogger(__name__)


class CopyWindowsWebContentBase(SubscriptionAction):
	"""Base class to copy web content for Windows servers"""
	def __init__(self):
		# Whether to check if copied file exists on source server before
		# running rsync command for better error reporting and ability to skip
		# files/directories if they don't exist on source server
		self._check_source_file_exists = False

	def get_description(self):
		return messages.COPY_WEB_FILES_FROM_WINDOWS_SERVERS

	def get_failure_message(self, global_context, subscription):
		"""
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		return messages.FAILED_COPY_WEB_FILES_FOR_SUBSCRIPTION % subscription.name

	def is_critical(self):
		"""If action is critical or not

		If action is critical and it failed for a subscription, migration tool
		won't run the next operations for the subscription.

		:rtype: bool
		"""
		return False

	def get_multithreading_properties(self):
		"""
		:rtype: parallels.core.actions.utils.multithreading_properties.MultithreadingProperties
		"""
		return MultithreadingProperties(can_use_threads=True)

	def filter_subscription(self, global_context, subscription):
		"""
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		source_server = subscription.web_source_server
		if source_server is None:
			return False
		webcontent_transfer_option = config_utils.get_option(source_server.node_id, 'copy-web-content', 'full')
		if webcontent_transfer_option == 'full':
			return subscription_filter.windows_with_virtual_hosting(subscription)
		else:
			logger.info(
				messages.LOG_NO_NEED_TO_COPY_WEBCONTENT_BECAUSE_DISABLED_BY_USER,
				subscription.name
			)

	def run(self, global_context, subscription):
		"""
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		files = self._list_files_to_copy(global_context, subscription)
		logger.debug(messages.DEBUG_FILES_TO_COPY.format(subscription_name=subscription.name, files=repr(files)))
		self._copy_files_windows(
			global_context,
			subscription,
			files,
			subscription.source_web_ip,
			subscription.web_target_server,
			subscription.web_source_server
		)

	def _copy_files_windows(self, global_context, subscription, files, source_ip, target_node, source_server):
		"""
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:type files: list[parallels.core.utils.paths.copy_web_content.CopyWebContentItem]
		"""

		rsync_additional_args = migrator_config.read_rsync_additional_args(global_context.config)
		apply_hosting_analyser_strategy(global_context, subscription, rsync_additional_args)

		rsync = global_context.get_rsync(source_server, target_node, source_ip)
		for item in files:
			logger.debug(
				messages.DEBUG_START_COPY_FILES.format(subscription_name=subscription.name, files_item=repr(item))
			)
			if not self._check_item_exists_on_source(
				global_context, item, source_server, subscription
			):
				continue
			if self._is_absent_by_default(item.target_path):
				with target_node.runner() as runner:
					runner.mkdir(TargetWebPathConverter().expand(item.target_path, target_node))
			try:
				rsync.sync(
					source_path=self._get_source_rsync_converter().expand(item.source_path, source_server),
					target_path=PathWindowsRsyncAdapter(TargetWebPathConverter()).expand(
						item.target_path, target_node
					),
					exclude=item.exclude,
					rsync_additional_args=rsync_additional_args
				)
			except Exception as e:
				logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise MigrationError((
					messages.RSYNC_FAILED_COPY_FILES_FROM_SOURCE) % (
					subscription.web_source_server.description(),
					subscription.web_target_server.description(),
					str(e)
				))

	def _check_item_exists_on_source(self, global_context, item, source_server, subscription):
		"""
		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		path_converter = self._get_source_converter(global_context, source_server)
		if path_converter is None:
			# Can not perform the check because don't know absolute path on the source server.
			# So, simply skip the check.
			return True

		source_path = path_converter.expand(item.source_path, source_server)

		with source_server.runner() as source_runner:
			if not windows_utils.file_exists(source_runner, source_path):
				if item.skip_if_source_not_exists:
					logger.debug(
						messages.FILE_DIRECTORY_S_SUBSCRIPTION_S_DOES % (
							source_path, subscription.name,
							source_server.description()
						)
					)
					return False
				else:
					raise MigrationNoRepeatError(
						messages.COPY_WEB_CONTENT_FAILED_FOR_SUBSCRIPTION % (
							subscription.name, source_path,
							source_server.description()
						)
					)
			else:
				return True

	@staticmethod
	def _get_source_rsync_converter():
		"""Get converter from abstract source path to concrete path for rsync

		:rtype: parallels.core.utils.paths.copy_web_content.BaseWebPathConverter
		"""
		return SourceRsyncVhostWebPathConverter()

	def _get_source_converter(self, global_context, source_server):
		"""Get converter from abstract source path to concrete path

		Note: this function could return None if we know path only rsync path, not absolute path

		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:rtype: parallels.core.utils.paths.copy_web_content.BaseWebPathConverter | None
		"""
		source_vhosts_dir = self._get_source_vhosts_dir(global_context, source_server)

		if source_vhosts_dir is None:
			# We don't know where virtual hosts directory is located on the source server.
			# So, paths can not be converted
			return None
		else:
			return SourceWebPathConverter(source_vhosts_dir)

	def _get_source_vhosts_dir(self, global_context, source_server):
		"""Get directory where virtual hosts data is located.

		Returns None by default. That is the case when we don't know where virtual hosts directory is located,
		for example if we don't have direct access to the source server, but have only manually configured
		rsync server.

		Override in child classes to perform additional checks for file existence
		before running rsync for better error reporting.

		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:rtype: str | None
		"""
		return None

	def _list_files_to_copy(self, global_context, subscription):
		"""Make a list of source server directories to be transferred.

		Return a list of (source directory -> destination directory) mappings.
		Override in child classes.

		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:rtype: list[parallels.core.utils.paths.copy_web_content.CopyWebContentItem]
		"""
		raise NotImplementedError()

	@staticmethod
	def _is_absent_by_default(path):
		"""Check whether specified web path is absent by default on target server.

		If so - we need to create directory for content before running rsync.

		:type path: parallels.core.utils.web_paths.WebHostingPath
		"""
		return isinstance(
			path, (
				web_paths.WebspaceSSLDocumentRoot,
				web_paths.WebspaceMainDomainPrivate,
				web_paths.SitePrivate,
				web_paths.WebspacePathTemplate,
				web_paths.SitePathTemplate
			)
		)


class SourceWebPathConverter(BaseWebPathConverter):
	"""Class to convert abstract path descriptor to concrete path on source server"""

	def __init__(self, vhost_dir):
		self._vhost_dir = vhost_dir

	def expand(self, path, server):
		"""Convert abstract path descriptor to concrete absolute path for source server

		:type path: parallels.core.utils.web_paths.WebHostingPath
		:rtype: str | unicode
		"""
		if isinstance(path, web_paths.AbsolutePath):
			return path.absolute_path
		if isinstance(path, web_paths.VirtualHostsPath):
			return ntpath.join(self._vhost_dir, path.relative_path)
		else:
			assert False, messages.UNSUPPORTED_SOURCE_WEB_PATH_TYPE


class SourceRsyncVhostWebPathConverter(BaseWebPathConverter):
	"""Class to convert abstract path descriptor to concrete path for rsync on source server"""

	def expand(self, path, server):
		"""Convert abstract path descriptor to concrete absolute path for source server

		:type path: parallels.core.utils.web_paths.WebHostingPath
		:rtype: str | unicode
		"""
		if isinstance(path, web_paths.VirtualHostsPath):
			return 'vhosts/%s' % path.relative_path.replace('\\', '/')
		else:
			assert False, messages.UNSUPPORTED_SOURCE_WEB_PATH_TYPE


class TargetWebPathConverter(BaseWebPathConverter):
	"""Class to convert abstract path descriptor to concrete path for rsync on target server"""

	def expand(self, path, server):
		"""Convert abstract path descriptor to concrete absolute path for target server

		:type path: parallels.core.utils.web_paths.WebHostingPath
		:rtype: str | unicode
		"""
		if isinstance(path, web_paths.AbsolutePath):
			return path.absolute_path
		elif isinstance(path, web_paths.WebspacePath):
			return self._expand_webspace_path(path, server)
		elif isinstance(path, web_paths.SitePath):
			return self._expand_site_path(path, server)
		else:
			assert False, messages.UNSUPPORTED_SOURCE_WEB_PATH_TYPE

	def _expand_webspace_path(self, path, server):
		"""
		:type path: parallels.core.utils.paths.web_paths.WebspacePath
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		name_idna = path.webspace.name.encode('idna')
		webspace_root_path = ntpath.join(server.vhosts_dir, name_idna)
		webpace_wwwroot_path = ntpath.join(webspace_root_path, path.webspace.www_root)
		dot_plesk_dir = ntpath.join(webspace_root_path, '.plesk')

		if isinstance(path, web_paths.WebspaceRoot):
			return webspace_root_path
		elif isinstance(path, web_paths.WebspaceDocumentRoot):
			return webpace_wwwroot_path
		elif isinstance(path, web_paths.WebspaceSSLDocumentRoot):
			return ntpath.join(webspace_root_path, 'httpsdocs', name_idna)
		elif isinstance(path, web_paths.WebspaceMainDomainPrivate):
			return ntpath.join(webspace_root_path, 'private', name_idna)
		elif isinstance(path, web_paths.WebspaceLogs):
			return ntpath.join(webspace_root_path, 'logs')
		elif isinstance(path, web_paths.WebspaceMainDomainStatistics):
			return ntpath.join(dot_plesk_dir, 'statistics', name_idna)
		elif isinstance(path, web_paths.WebspaceSecurityFile):
			return ntpath.join(dot_plesk_dir, '.Security')
		elif isinstance(path, web_paths.WebspaceAdditionalSecurityFile):
			return ntpath.join(dot_plesk_dir, path.security_file_path)
		elif isinstance(path, web_paths.WebspacePathTemplate):
			return self._expand_webspace_template(path, server)
		else:
			assert False, messages.UNSUPPORTED_SOURCE_WEB_PATH_TYPE

	def _expand_site_path(self, path, server):
		"""
		:type path: parallels.core.utils.paths.web_paths.SitePath
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		webspace_name_idna = path.webspace.name.encode('idna')
		site_name_idna = path.site.name.encode('idna')
		webspace_root_path = ntpath.join(server.vhosts_dir, webspace_name_idna)
		dot_plesk_dir = ntpath.join(webspace_root_path, '.plesk')

		if isinstance(path, web_paths.SiteDocumentRoot):
			return ntpath.join(webspace_root_path, path.site.www_root)
		elif isinstance(path, web_paths.SitePrivate):
			return ntpath.join(webspace_root_path, 'private', site_name_idna)
		elif isinstance(path, web_paths.SiteStatistics):
			return ntpath.join(dot_plesk_dir, 'statistics', site_name_idna)
		elif isinstance(path, web_paths.SitePathTemplate):
			return self._expand_site_template(path, server)
		else:
			assert False, messages.UNSUPPORTED_SOURCE_WEB_PATH_TYPE

	def _expand_webspace_template(self, path, server):
		"""
		:type path: parallels.core.utils.web_paths.WebspacePathTemplate
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		variable_paths = {
			'webspace_root': web_paths.WebspaceRoot,
			'document_root': web_paths.WebspaceDocumentRoot,
			'ssl_document_root': web_paths.WebspaceSSLDocumentRoot,
			'main_domain_private': web_paths.WebspaceMainDomainPrivate,
			'logs': web_paths.WebspaceLogs,
			'main_domain_statistics': web_paths.WebspaceMainDomainStatistics,
		}
		variables = {}
		for var_name, path_class in variable_paths.iteritems():
			variables[var_name] = self.expand(path_class(path.webspace), server)
		variables['webspace'] = path.webspace.name
		variables['webspace_idn'] = path.webspace.name.encode('idna')

		return path.template.format(**variables)

	def _expand_site_template(self, path, server):
		"""
		:type path: parallels.core.utils.web_paths.SitePathTemplate
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		webspace_variable_paths = {
			'webspace_root': web_paths.WebspaceRoot,
			'webspace_document_root': web_paths.WebspaceDocumentRoot,
			'webspace_ssl_document_root': web_paths.WebspaceSSLDocumentRoot,
			'webspace_logs': web_paths.WebspaceLogs,
		}
		site_variable_paths = {
			'document_root': web_paths.SiteDocumentRoot,
			'private': web_paths.SitePrivate,
			'statistics': web_paths.SiteStatistics,
		}
		variables = {}
		for var_name, path_class in webspace_variable_paths.iteritems():
			variables[var_name] = self.expand(
				path_class(path.webspace), server
			)
		for var_name, path_class in site_variable_paths.iteritems():
			variables[var_name] = self.expand(
				path_class(path.webspace, path.site), server
			)
		variables['webspace'] = path.webspace.name
		variables['webspace_idn'] = path.webspace.name.encode('idna')
		variables['site'] = path.site.name
		variables['site_idn'] = path.site.name.encode('idna')

		return path.template.format(**variables)


class PathWindowsRsyncAdapter(BaseWebPathConverter):
	"""Wrapper around another path converter, adding conversion from Windows native to cygwin rsync format

	So all paths like:
	c:\windows\system32
	will be converted to
	/cygdrive/c/windows/system32
	"""

	def __init__(self, converter):
		"""
		:type converter: parallels.core.utils.paths.copy_web_content.BaseWebPathConverter
		"""
		self._converter = converter

	def expand(self, path, server):
		"""Convert abstract path descriptor to concrete absolute path, in rsync (cygwin) format

		:type path: parallels.core.utils.web_paths.WebHostingPath
		:rtype: str | unicode
		"""
		return windows_utils.convert_path_to_cygwin(self._converter.expand(path, server))