import posixpath
import logging

from parallels.core import messages
from parallels.core.actions.base.subscription_action import SubscriptionAction
from parallels.core import migrator_config
from parallels.core.actions.utils.multithreading_properties import MultithreadingProperties
from parallels.core.utils import migrator_utils
from parallels.core.utils import subscription_filter
from parallels.core import MigrationError
from parallels.core.utils.entity import Entity
from parallels.core.utils.hosting_analyser_utils import apply_hosting_analyser_strategy
from parallels.core.utils.paths import web_paths
from parallels.core.utils.plesk_utils import convert_wildcard_to_path
from parallels.core.utils.common import default


logger = logging.getLogger(__name__)


class CopyWebContentItem(Entity):
	def __init__(self, source_path, target_path, skip_if_source_not_exists=False, exclude=None):
		"""
		:type source_path: parallels.core.utils.web_paths.WebHostingPath
		:type target_path: parallels.core.utils.web_paths.WebHostingPath
		:type skip_if_source_not_exists: bool
		:type exclude: list[basestring]
		"""
		self._source_path = source_path
		self._target_path = target_path
		self._skip_if_source_not_exists = skip_if_source_not_exists
		self._exclude = default(exclude, [])

	@property
	def source_path(self):
		"""
		:rtype: parallels.core.utils.web_paths.WebHostingPath
		"""
		return self._source_path

	@property
	def target_path(self):
		"""
		:rtype: parallels.core.utils.web_paths.WebHostingPath
		"""
		return self._target_path

	@property
	def skip_if_source_not_exists(self):
		"""
		:rtype: bool
		"""
		return self._skip_if_source_not_exists

	@property
	def exclude(self):
		"""
		:rtype: list[basestring]
		"""
		return self._exclude


class CopyUnixWebContentBase(SubscriptionAction):
	"""Base class to copy web content for Unix servers"""

	def get_description(self):
		"""
		:rtype: basestring
		"""
		return messages.COPY_WEB_FILES_FROM_UNIX_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_1 % 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
		"""
		return subscription_filter.unix_with_virtual_hosting(
			subscription
		) and subscription.web_source_server is not None

	def run(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
		target_server = subscription.web_target_server

		user_mapping = {}
		group_mapping = {}

		if source_server.apache_user is not None:
			if source_server.apache_user != target_server.apache_user:
				user_mapping[source_server.apache_user] = target_server.apache_user
			if source_server.apache_group != target_server.apache_group:
				group_mapping[source_server.apache_group] = target_server.apache_group

		tocopy = self._list_files_to_copy(global_context, subscription)
		logger.debug('Copy the following web files: %r', tocopy)
		rsync_additional_args = migrator_config.read_rsync_additional_args(global_context.config)
		apply_hosting_analyser_strategy(global_context, subscription, rsync_additional_args)

		key_info = global_context.ssh_key_pool.get(source_server, target_server)
		with \
			source_server.runner() as runner_source, \
			target_server.runner() as runner_target:
				try:
					for item in tocopy:
						migrator_utils.copy_directory_content_unix(
							source_server.ip(),
							source_server.user(),
							runner_source,
							runner_target,
							SourceWebPathConverter().expand(item.source_path, source_server),
							TargetWebPathConverter().expand(item.target_path, target_server),
							key_info.key_pathname,
							item.exclude,
							item.skip_if_source_not_exists,
							rsync_additional_args=rsync_additional_args,
							user_mapping=user_mapping,
							group_mapping=group_mapping,
							source_rsync_bin=source_server.rsync_bin
						)
				except Exception as e:
					logger.debug(u"Exception: ", exc_info=e)
					raise MigrationError(
						messages.RSYNC_FAILED_COPY_FILES_FROM_SOURCE_1 % (
							source_server.description(), target_server.description(), str(e)
						)
					)

	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.actions.content.web.copy_unix_content_base.CopyWebContentItem]
		"""
		raise NotImplementedError()


class BaseWebPathConverter(object):
	"""Class to convert abstract path descriptor to concrete absolute path"""

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

		:type path: parallels.core.utils.web_paths.WebHostingPath
		:rtype: basestring
		"""
		raise NotImplementedError()


class SourceWebPathConverter(BaseWebPathConverter):
	"""Class to convert abstract path descriptor to concrete absolute path 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: basestring
		"""
		if isinstance(path, web_paths.AbsolutePath):
			return path.absolute_path
		else:
			assert False, "Unsupported source web path type"


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

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

		:type path: parallels.core.utils.web_paths.WebHostingPath
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		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, "Unsupported target web path type"

	def _expand_webspace_path(self, path, server):
		"""
		:type path: parallels.core.utils.web_paths.WebspacePath
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		webspace_root_path = posixpath.join(
			server.vhosts_dir, path.webspace.name.encode('idna')
		)
		webpace_wwwroot_path = posixpath.join(webspace_root_path, path.webspace.www_root)
		webspace_system_path = posixpath.join(
			server.vhosts_dir, 'system', path.webspace.name.encode('idna')
		)
		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.WebspaceCGIBin):
			return posixpath.join(webpace_wwwroot_path, 'cgi-bin')
		elif isinstance(path, web_paths.WebspaceStatistics):
			return posixpath.join(webspace_system_path, 'statistics')
		elif isinstance(path, web_paths.WebspaceLogs):
			return posixpath.join(webspace_system_path, 'logs')
		elif isinstance(path, web_paths.WebspaceProtectedDirs):
			return posixpath.join(webspace_system_path, 'pd')
		elif isinstance(path, web_paths.WebspaceAnonFTPPub):
			return posixpath.join(webspace_root_path, 'anon_ftp/pub')
		elif isinstance(path, web_paths.WebspaceAnonFTPIncoming):
			return posixpath.join(webspace_root_path, 'anon_ftp/incoming')
		elif isinstance(path, web_paths.WebspaceWebUser):
			return posixpath.join(webspace_root_path, 'web_users/%s' % path.webuser_name)
		elif isinstance(path, web_paths.WebspacePathTemplate):
			return self._expand_webspace_template(path, server)
		else:
			assert False, "Unsupported target web path type"

	def _expand_site_path(self, path, server):
		"""
		:type path: parallels.core.utils.web_paths.SitePath
		:type server: parallels.core.connections.plesk_server.PleskServer
		:rtype: basestring
		"""
		site_system_path = posixpath.join(
			server.vhosts_dir, 'system',
			convert_wildcard_to_path(path.site.name.encode('idna'))
		)
		site_wwwroot_path = posixpath.join(
			server.vhosts_dir, path.webspace.name.encode('idna'), path.site.www_root
		)
		if isinstance(path, web_paths.SiteDocumentRoot):
			return site_wwwroot_path
		elif isinstance(path, web_paths.SiteCGIBin):
			return posixpath.join(site_wwwroot_path, 'cgi-bin')
		elif isinstance(path, web_paths.SiteStatistics):
			return posixpath.join(site_system_path, 'statistics')
		elif isinstance(path, web_paths.SiteLogs):
			return posixpath.join(site_system_path, 'logs')
		elif isinstance(path, web_paths.SiteProtectedDirs):
			return posixpath.join(site_system_path, 'pd')
		elif isinstance(path, web_paths.SitePathTemplate):
			return self._expand_site_template(path, server)
		else:
			assert False, "Unsupported target 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,
			'cgi_bin': web_paths.WebspaceCGIBin,
			'statistics': web_paths.WebspaceStatistics,
			'logs': web_paths.WebspaceLogs,
			'protected_dirs': web_paths.WebspaceProtectedDirs,
			'anon_ftp_pub': web_paths.WebspaceAnonFTPIncoming,
			'anon_ftp_incoming': web_paths.WebspaceAnonFTPIncoming,
		}
		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_cgi_bin': web_paths.WebspaceCGIBin,
			'webspace_statistics': web_paths.WebspaceStatistics,
			'webspace_logs': web_paths.WebspaceLogs,
			'webspace_protected_dirs': web_paths.WebspaceProtectedDirs,
			'webspace_anon_ftp_pub': web_paths.WebspaceAnonFTPIncoming,
			'webspace_anon_ftp_incoming': web_paths.WebspaceAnonFTPIncoming,
		}
		site_variable_paths = {
			'document_root': web_paths.SiteDocumentRoot,
			'cgi_bin': web_paths.SiteCGIBin,
			'statistics': web_paths.SiteStatistics,
			'logs': web_paths.SiteLogs,
			'protected_dirs': web_paths.SiteProtectedDirs,
		}
		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)
