import posixpath
import json
import logging
from collections import OrderedDict

from parallels.core import messages, CommandExecutionError
from parallels.core import MigrationError
from parallels.core.utils.common import ilen
from parallels.core import migrator_config
from parallels.core.utils.hosting_analyser_utils import apply_hosting_analyser_strategy
from parallels.core.utils.paths.mail_paths import MailboxDirectory, MailDomainDirectory


logger = logging.getLogger(__name__)


class CopyMailRsync(object):
	"""Copy mail messages with rsync to target Unix server"""

	def __init__(self, source_mail_directory):
		"""Object constructor

		Arguments:
			source_mail_directory - constructor of class which provides
				paths to directories with mail messages on source server,
				subclass of SourceMailDirectory
		"""
		self.source_mail_directory = source_mail_directory

	def copy_mail(self, global_context, migrator_server, subscription, issues):
		"""
		Copy mail content of given subscription by rsync

		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		if subscription.converted_mail_backup is None:
			logger.debug(messages.SUBSCRIPTION_S_IS_NOT_PRESENTED_MAIL_1, subscription.name)
			return

		if subscription.mail_source_server is None:
			logger.debug(messages.SOURCE_MAIL_SERVER_IS_NOT_DEFINED, subscription.name)
			return

		for domain in subscription.converted_mail_backup.iter_domains():
			if domain.mailsystem is None:
				logger.debug(messages.SKIP_COPYING_MAIL_CONTENT_FOR_DOMAIN_1, domain.name)
				continue
			if ilen(domain.iter_mailboxes()) == 0:
				logger.debug(messages.SKIP_COPYING_MAIL_CONTENT_FOR_DOMAIN_2, domain.name)
				continue

			self._copy_domain_mail(global_context, subscription, domain)
			self._repair_file_permissions(subscription, domain)
			self._recalculate_maildirsize(subscription, domain)

	def _copy_domain_mail(self, global_context, subscription, domain):
		"""Transfer mail content of the specified domain to the target server.
		
		When copying mail from Plesk to Plesk, mail content is transferred per
		domain, because mail directory structure is the same.

		:type global_context: parallels.core.global_context.GlobalMigrationContext
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		source_server = subscription.mail_source_server
		target_server = subscription.mail_target_server
		domain_name = domain.name.encode('idna').lower()
		rsync_additional_args = migrator_config.read_rsync_additional_args(global_context.config)
		apply_hosting_analyser_strategy(global_context, subscription, rsync_additional_args)

		ssh_key_info = global_context.ssh_key_pool.get(source_server, target_server)

		source_dir = self.source_mail_directory(subscription, domain)
		for source_path, target_location in source_dir.prepare():
			target_path = self._get_target_path(target_server, target_location, domain_name)

			rsync_command = self._get_rsync_args(
				source_server, source_path, target_path, ssh_key_info.key_pathname,
				rsync_additional_args=rsync_additional_args, source_dir=source_dir
			)
			self._run_rsync(source_server, target_server, rsync_command)

		source_dir.cleanup()

	@staticmethod
	def _run_rsync(source_server, target_server, rsync_command):
		try: 
			with target_server.runner() as runner_target:
				runner_target.run(u'/usr/bin/rsync', rsync_command)
		except CommandExecutionError as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			raise MigrationError(
				messages.RSYNC_FAILED_COPY_MAIL_CONTENT_FROM % (
					source_server.description(), target_server.description(), e.stderr
				)
			)
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			raise MigrationError(
				messages.RSYNC_FAILED_COPY_MAIL_CONTENT_FROM_1 % (
					source_server.description(), target_server.description(), str(e)
				)
			)

	@staticmethod
	def _get_rsync_args(
		source_server, source_path, target_path, key_pathname,
		rsync_additional_args, source_dir
	):
		"""Generate rsync parameters for transferring domain mail content."""
		if rsync_additional_args is None:
			rsync_additional_args = []
		rsync_options = [
			u"-a", u"-e",
			u'ssh -i %s -p %s -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no' % (
				key_pathname, source_server.settings().ssh_auth.port
			)
		]
		default_exclude_list = [u"maildirsize", u".qmail*", u"@attachments/"]
		mail_dir_exclude = source_dir.get_excluded_files()
		if mail_dir_exclude is None:
			mail_dir_exclude = []
		for opt in default_exclude_list + mail_dir_exclude:
			rsync_options.extend([u"--exclude", opt])

		source_uri = u"%s@%s:%s/" % (source_server.user(), source_server.ip(), source_path)
		transfer_paths = [source_uri, u"%s/" % target_path]
		return rsync_options + rsync_additional_args + transfer_paths

	def _get_target_path(self, target_server, target_location, domain_name):
		if isinstance(target_location, MailboxDirectory):
			return posixpath.join(
				target_server.mail_dir, domain_name,
				target_location.mailbox_name.encode('idna'))
		elif isinstance(target_location, MailDomainDirectory):
			return posixpath.join(target_server.mail_dir, domain_name)
		else:
			assert False

	@staticmethod
	def _repair_file_permissions(subscription, domain):
		"""

		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		target_server = subscription.mail_target_server

		with target_server.runner() as runner:
			target_path = posixpath.join(
				target_server.mail_dir,
				domain.name.encode('idna').lower()
			)
			path_args = dict(path=target_path)
			runner.sh('chown popuser:popuser -R {path}', path_args)
			runner.sh('chmod 750 {path}', path_args)

	@staticmethod
	def _recalculate_maildirsize(subscription, domain):
		"""
		After migration of mail content we have to recalculate mail usage in
		"maildirsize" file of mail directory. Otherwise, we could get
		incorrect mail user in the control panel, and mail quota limitations
		could work incorrectly.

		For example, we have a mailbox. It was initially migrated to target server.
		During DNS switch:
		- several messages were received by the source mail server
		- several other messages were received by the target mail server

		Then customer runs mail content resync, which merges messages from
		source and target server. "maildirsize" file on the target server
		becomes invalid, as new messages from the source server were put into
		the mailbox (and mail server does not know that). But we can't just
		take the file from the source server, because there are new messages
		that were received by the target server, but not the source server, and
		they are not included into "maildirsize" file calculation on the source
		server. So neither source, nor target "maildirsize" file is correct.
		We need Plesk backend to recalculate it. To do that, we call mailbox
		quota update.

		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		try:
			for mailbox in domain.iter_mailboxes():
				CopyMailRsync._recalculate_mailbox_maildir_size(mailbox, subscription)
		except Exception:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			raise MigrationError(
				messages.FAILED_RECALCULATE_MAIL_DIRECTORY_SIZE_MAIL)

	@staticmethod
	def _recalculate_mailbox_maildir_size(mailbox, subscription):
		"""Recalculate maildir size on single mailbox

		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		target_mail_server = subscription.mail_target_server
		target_plesk_server = subscription.panel_target_server

		if target_plesk_server.get_plesk_version() <= (11, 5):
			jsonquery = json.dumps({'domain': {
				'name': mailbox.domain_name.encode('idna'),
				'dom_aliases': [],
				'mails': []}
			})
		else:
			jsonquery = json.dumps(
				OrderedDict([
					(
						'domain',
						OrderedDict([
							('name', mailbox.domain_name.encode('idna')),
							('dom_aliases', []),
							('mails', []),
							('maillists', [])
						])
					)
				])
			)
		with target_mail_server.runner() as runner_target:
			# See json schema specified in Plesk SVN, file 'mail_db_data.sj'
			runner_target.sh(
				'echo {json} | /usr/local/psa/admin/bin/mailmng-core --set-mailbox-disk-quota '
				'--domain-name={domain_name} --mailname={mailbox_name} --disk-quota={quota}',
				dict(
					domain_name=mailbox.domain_name.encode('idna'),
					mailbox_name=mailbox.name,
					quota=str(mailbox.quota) if mailbox.quota is not None else '-1',
					json=jsonquery
				)
			)


class SourceMailDirectory(object):
	"""Provide paths to directories with mail messages on source server"""

	def __init__(self, subscription, domain):
		self.subscription = subscription
		self.domain = domain

	def prepare(self):
		"""Prepare mail directory on source server to copy with rsync
		
		Returns tuple with two items:
		- path to a directory on source server to copy for
		domain specified in constructor of that class.
		- relative path to a directory on target server

		This function may perform some conversion on files on the 
		source server, and return path to some temporary converted 
		directory which should be removed in cleanup function

		:rtype: list[tuple(str, parallels.core.content.mail.rsync.MailHostingPath]]
		"""
		raise NotImplementedError()

	def cleanup(self):
		"""Clean up temporary files when copy is finished"""
		pass

	def get_excluded_files(self):
		"""Return a list of files that should not be transferred

		:rtype: list[basestring]
		"""
		return []
