from parallels.core import messages
import logging
import time
from parallels.core import imapsync

from parallels.core.utils import unix_utils
from parallels.core.checking import Issue, Problem
from parallels.core.utils.common import is_empty
from parallels.core.utils.common import group_by_id
from parallels.core.utils.common import safe_string_repr
from parallels.core.migrator_config import MailImapEncryption

logger = logging.getLogger(__name__)


class CopyMailImapsync(object):
	def __init__(self, imap_supported):
		self.imap_supported = imap_supported

	def copy_mail(self, global_context, migrator_server, subscription, issues):
		"""Copy mail content of a single Plesk server over a POP protocol:
		IMAP, if imap_supported is True,
		POP3 otherwise
		"""

		if subscription.converted_mail_backup is None:
			logger.debug(messages.SUBSCRIPTION_S_IS_NOT_PRESENTED_MAIL_2, subscription.name)
			return
		
		mailboxes_count = len([
			mailbox
			for domain in subscription.converted_mail_backup.iter_domains()
			for mailbox in domain.iter_mailboxes()
		])

		if mailboxes_count == 0:
			logger.debug(
				messages.SUBSCRIPTION_NO_MAILBOXES_SO_THERE_IS)
			return 

		if not self._check_mailsystem_disabled(subscription, issues):
			return

		if not self._check_subscription_suspended(subscription, issues):
			return

		for domain, raw_domain in self._iter_domains(subscription):
			if not self._check_domain_mailsystem_disabled(domain, issues):
				continue

			for mailbox, raw_mailbox in self._iter_mailboxes(
				domain, raw_domain
			):
				self._copy_single_mailbox(
					subscription, domain, mailbox, raw_mailbox, migrator_server, issues
				)

	@staticmethod
	def _iter_domains(subscription):
		"""Iterate over list of pairs (domain, raw_domain)"""
		raw_domains = group_by_id(
			subscription.raw_mail_backup.iter_domains(), lambda d: d.name
		)
		for domain in subscription.converted_mail_backup.iter_domains():
			yield (domain, raw_domains[domain.name])

	@staticmethod
	def _iter_mailboxes(domain, raw_domain):
		"""Iterate over list of pairs (mailbox, raw_mailbox)"""
		raw_mailboxes = group_by_id(
			raw_domain.iter_mailboxes(), lambda m: m.name
		)
		for mailbox in domain.iter_mailboxes():
			yield (mailbox, raw_mailboxes[mailbox.name])

	def _copy_single_mailbox(
		self, subscription, domain, mailbox, raw_mailbox, migrator_server, issues
	):
		source_server = subscription.mail_source_server

		def session_path(filename):
			return migrator_server.get_session_file_path(filename)

		email_idna = mailbox.name + "@" + mailbox.domain_name.encode("idna")
		email = mailbox.name + "@" + mailbox.domain_name
		file_suffix = '%s_%s' % (mailbox.domain_name.encode('idna'), mailbox.name.encode('idna'))

		source_password_file_name = session_path('mail-password-source_%s.txt' % file_suffix)
		target_password_file_name = session_path('mail-password-target_%s.txt' % file_suffix)
		mailsync_log_file_name = session_path('mailsync_%s.log' % file_suffix)

		if not self._check_mailbox_password(mailbox, raw_mailbox, issues):
			return
		
		logger.debug(
			messages.COPY_CONTENT_MAILBOX_EMAIL_S_FOR % (
				email, mailsync_log_file_name,
			)
		)

		with migrator_server.runner() as runner:
			runner.upload_file_content(
				source_password_file_name,
				raw_mailbox.password
			)
			runner.upload_file_content(
				target_password_file_name,
				mailbox.password
			)

		command_arguments = dict(
			source_ip=subscription.source_mail_ip,
			target_ip=subscription.mail_target_server.ip(),
			email=email_idna,
			source_passfile=source_password_file_name,
			target_passfile=target_password_file_name
		)

		if source_server.mail_settings.custom_mail_copy_content_command:
			cmd = unix_utils.format_command(
				source_server.mail_settings.custom_mail_copy_content_command,
				**command_arguments
			)
		else:
			if self.imap_supported:
				cmd_optional_args = self._get_optional_args(subscription)
				source_encryption_settings_mapping = {
					MailImapEncryption.NONE: '',
					MailImapEncryption.SSL: ' --ssl1',
					MailImapEncryption.TLS: ' --tls1'
				}
				imapsync.install_if_not_installed()
				cmd = unix_utils.format_command(
					# regexflag option is necessary for migration of messages
					# with Forwarded flag from MailEnable (running on some
					# source Plesks) to Courier (used at PPA) without this
					# replacement messages that have this flag will not be
					# copied: Courier answers "NO Error in IMAP command
					# received by server." to any attempt to add a message with
					# '\$Forwarded' flag (reported by MailEnable), still
					# Courier accepts '$Forwarded' (without leading backslash)
					# flag
					u"imapsync --noreleasecheck --noexpunge "
					u"--regexflag 's/\\\\\\$Forwarded/\\$Forwarded/g' "
					u"--host1 {source_ip} --user1 {email} --passfile1 {source_passfile} "
					u"--host2 {target_ip} --user2 {email} --passfile2 {target_passfile}",
					**command_arguments
				)

				cmd += cmd_optional_args
				cmd += source_encryption_settings_mapping[
					source_server.mail_settings.mail_imap_encryption
				]
			else:
				cmd = unix_utils.format_command(
					u"pop2imap "
					u"--host1 {source_ip} --user1 {email} --passfile1 {source_passfile} "
					u"--host2 {target_ip} --user2 {email} --passfile2 {target_passfile}",
					**command_arguments
				)

		with migrator_server.runner() as runner:
			exit_code, stdout, stderr = runner.sh_unchecked(cmd)
		output = stdout + stderr

		with open(mailsync_log_file_name, 'a') as f:
			f.write((u"""
{date} {cmd}

""".format(date=time.asctime(), cmd=cmd)
			).encode('utf-8'))
			f.write((u"Exit code: %s, command output:\n%s" % (exit_code, safe_string_repr(output),)).encode('utf-8'))

		with migrator_server.runner() as runner:
			for f in [source_password_file_name, target_password_file_name]:
				runner.remove_file(f)

		if exit_code != 0:
			issues.append(Issue(
				Problem(
					'',
					Problem.ERROR,
					u"Unable to copy mail content of mailbox '{email}'. Command exit code: {exit_code}, last 5 lines of output:\n{output}".format(
						email=email,
						exit_code=exit_code,
						output=safe_string_repr("\n".join(output.strip().split("\n")[-5:]))
					),
				),
				solution=(
					messages.RESOLVE_ERRORS_AND_RUN_TOOL_COPYMAILCONTENT.format(
						mailsync_log=mailsync_log_file_name,
					)
				)
			))

	@staticmethod
	def _get_optional_args(subscription):
		source_server = subscription.mail_source_server
		source_settings = source_server.mail_settings
		target_server = subscription.mail_target_server
		cmd_optional_args = ''

		# source separator
		if source_settings.mailbox_name_separator is None:
			separator = '/'
		else:
			separator = source_settings.mailbox_name_separator
		cmd_optional_args += unix_utils.format_command(u" --sep1 {0}", separator)

		# source prefix
		if source_settings.mailbox_name_prefix is None:
			prefix = ''
		else:
			prefix = source_settings.mailbox_name_prefix
		cmd_optional_args += unix_utils.format_command(u" --prefix1 {0}", prefix)
	
		if target_server.is_windows():
			# Defaults that must work for the only windows-based MTA supported
			# by PPA, SmarterMail. If some other windows-based MTA will be
			# supported by PPA, this may stop working.
			cmd_optional_args += u" --sep2 '/' --prefix2 ''"

		return cmd_optional_args

	@staticmethod
	def _check_mailsystem_disabled(subscription, issues):
		"""Check if mailsystem is disabled: in that case we can't copy mail"""
		backup_subscription = subscription.converted_mail_backup

		if backup_subscription.mailsystem is None:
			return False

		if not backup_subscription.mailsystem.enabled:
			issues.append(
				Issue(
					Problem(
						'',
						Problem.WARNING,
						messages.MAIL_SERVICE_SUBSCRIPTION_IS_DISABLED_MIGRATION,
					),
					solution=(
						messages.ENABLE_MAIL_SERVICE_SOURCE_PANEL_FOR)
				)
			)
			return False

		return True

	@staticmethod
	def _check_subscription_suspended(subscription, issues):
		"""Check if subscription is suspended: in that case we can't copy mail"""
		try:
			is_suspended_target = subscription.suspended_target
			if subscription.suspended_source or is_suspended_target:
				if subscription.suspended_source and is_suspended_target:
					where = messages.BOTH_SOURCE_PANEL_AND_TARGET_PANEL 
				elif not subscription.suspended_source and is_suspended_target:
					where = u"target panel"
				elif subscription.suspended_source and not is_suspended_target:
					where = u"source panel"
				else:
					assert False
				
				issues.append(
					Issue(
						Problem(
							'',
							Problem.WARNING,
							messages.SUBSCRIPTION_IS_SUSPENDED_S_MIGRATION_TOOL % (where,),
						),
						solution=(
							messages.MIGRATE_MAIL_CONTENT_SUBSCRIPTION_ACTIVATE_IT)
					)
				)
				return False
		except Exception as e:
			logger.error(
				messages.FAILED_CHECK_WHETHER_SUBSCRIPTION_S_IS,
				subscription.name, e
			)
			logger.debug(u'Exception:', exc_info=e)

		return True

	@staticmethod
	def _check_domain_mailsystem_disabled(domain, issues):
		if not domain.is_enabled:
			issues.append(
				Problem(
					'',
					Problem.WARNING,
					messages.UNABLE_COPY_MAIL_CONTENT_S_DOMAIN % domain.name,
				),
				solution=(
					messages.MIGRATE_MAIL_CONTENT_MAILBOXS_ACTIVATE_DOMAIN)
			)
			return False

		return True

	@staticmethod
	def _check_mailbox_password(mailbox, raw_mailbox, issues):
		"""Check if mailbox password is not empty and is plain text"""
		if is_empty(mailbox.password) or is_empty(raw_mailbox.password):
			logger.debug(messages.MAILBOX_EMPTY_PASSWORD_EMAIL_S_SKIPPING % mailbox.full_name)
			return False

		if not mailbox.enabled:
			logger.debug(messages.MAILBOX_IS_DISABLED_EMAIL_S_SKIPPING % mailbox.full_name)
			return False

		if mailbox.password_type != 'plain':
			issues.append(Issue(
				Problem(
					'',
					Problem.WARNING,
					messages.UNABLE_COPY_MAIL_CONTENT_MAILBOX_PASSWORD,
				),
				solution=messages.CHANGE_MAILBOX_PASSWORD_AND_RUN_TOOL))
			return False

		return True
