import posixpath
import json
import logging
from collections import OrderedDict

from parallels.common import run_command
from parallels.common import MigrationError
from parallels.utils import ilen
from parallels.common import migrator_config
from parallels.hosting_analyser.hosting_analyser import HostingAnalyser

logger = logging.getLogger(__name__)


class CopyMailRsync(object):
	"""Copy mail messages with rsync to target Plesk/PPA 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.common.global_context.GlobalMigrationContext
		:type subscription: parallels.common.migrated_subscription.MigratedSubscription
		"""
		if subscription.converted_mail_backup is None:
			logger.debug("Subscription '%s' is not presented on mail server, do not copy mail content", subscription.name)
			return

		if subscription.mail_source_server is None:
			logger.debug("Source mail server is not defined for subscription '%s', do not copy mail content", subscription.name)
			return

		for domain in subscription.converted_mail_backup.iter_domains():
			if domain.mailsystem is None:
				logger.debug(u"Skip copying mail content for the domain %s which has no mail hosting", domain.name)
				continue
			if ilen(domain.iter_mailboxes()) == 0:
				logger.debug(u"Skip copying mail content for the domain %s which has no mailboxes", 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.common.global_context.GlobalMigrationContext
		:type subscription: parallels.common.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)
		hosting_analyser = HostingAnalyser(global_context.migrator_server)
		if rsync_additional_args.count('-z') == 0 and hosting_analyser.is_compression_needed(subscription):
			rsync_additional_args.append('-z')

		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():
			if isinstance(target_location, TargetMailboxDirectory): 
				target_path = posixpath.join(
					target_server.mail_dir,
					domain_name,
					target_location.mailbox_name.encode('idna')
				)
			elif isinstance(target_location, TargetMailDomainDirectory):
				target_path = posixpath.join(
					target_server.mail_dir,
					domain_name
				)
			else:
				assert False

			rsync_command = self._get_rsync_args(
				source_server, source_path, target_path, ssh_key_info.key_pathname,
				rsync_additional_args=rsync_additional_args
			)
			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 run_command.HclRunnerException as e:
			logger.debug(u"Exception: ", exc_info=e)
			raise MigrationError((
				u"Rsync failed to copy mail content from the source (%s) to the target server (%s): %s\n"
				u"1. This could happen because of a network connection issue. "
				u"""Retry copying the mail content with the help of the "copy-mail-content" command.\n"""
				u"2. Check whether rsync is installed and configured on the source server."
			) % (source_server.description(), target_server.description(), e.stderr))
		except Exception as e:
			logger.debug(u"Exception: ", exc_info=e)
			raise MigrationError((
				u"Rsync failed to copy mail content from the source (%s) to the target server (%s): %s\n"
				u"1. This could happen because of a network connection issue. "
				"""Retry copying the mail content with the help of the "copy-mail-content" command.\n"""
				u"2. Check whether rsync is installed and configured on the source server."
			) % (source_server.description(), target_server.description(), str(e)))

	@staticmethod
	def _get_rsync_args(
		source_server, source_path, target_path, key_pathname,
		rsync_additional_args
	):
		if rsync_additional_args is None:
			rsync_additional_args = []

		"""Generate rsync parameters for transferring domain mail content."""
		return ([
			u"-a", u"-e", u"ssh -i %s -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no" % key_pathname,
			u"--exclude", u"maildirsize",
			u"--exclude", u".qmail*",
			u"--exclude", u"@attachments/"
		] + rsync_additional_args + [
			u"%s@%s:%s/" % (
				source_server.user(), source_server.ip(),
				source_path
			), 
			u"%s/" % (target_path,)
		])

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

		:type subscription: parallels.common.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 PPA.
		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.common.migrated_subscription.MigratedSubscription
		"""
		try:
			for mailbox in domain.iter_mailboxes():
				CopyMailRsync._recalculate_mailbox_maildir_size(mailbox, subscription)
		except Exception:
			logger.debug("Exception: ", exc_info=True)
			raise MigrationError(
				"Failed to recalculate mail directory size. Mail usage may become incorrect after migration"
			)

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

		:type subscription: parallels.common.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
		"""
		raise NotImplementedError()

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


class TargetMailDomainDirectory(object):
	pass


class TargetMailboxDirectory(object):
	def __init__(self, mailbox_name):
		self.mailbox_name = mailbox_name
