import logging
import time

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

logger = logging.getLogger(__name__)

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

	def copy_mail(self, 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("Subscription '%s' is not presented on mail server, do not copy mail content", 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(
				"Subscription has no mailboxes, so there is "
				"nothing to copy with imapsync"
			)
			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
				)

	def _iter_domains(self, 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])

	def _iter_mailboxes(self, 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

		source_password_file_name = session_path('mail-password-source.txt')
		target_password_file_name = session_path('mail-password-target.txt')
		mailsync_log_file_name = session_path('mailsync.log')

		if not self._check_mailbox_password(mailbox, raw_mailbox, issues):
			return
		
		logger.debug(
			u"Copy content of mailbox on E-mail '%s'. "
			u"For details see %s in migration session directory" % (
				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'
				}
				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}'. " 
					u"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=u"Resolve the errors and run the tool with the copy-mail-content command. For detailed log see {mailsync_log}. ".format(
					mailsync_log=mailsync_log_file_name,
				)
			))

	def _get_optional_args(self, 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

	def _check_mailsystem_disabled(self, 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 backup_subscription.mailsystem.enabled == False:
			issues.append(
				Issue(
					Problem(
						'',
						Problem.WARNING,
						u"Mail service on subscription is disabled. "
						u"The migration tool is not able to migrate mail content of subscriptions as POP3/IMAP services are disabled for such subscriptions.",
					),
					solution=u"Enable mail service on source panel for subscription to migrate mail content and run 'copy-mail-content' migration tool's command for the subscription."
				)
			)
			return False

		return True

	def _check_subscription_suspended(self, subscription, issues):
		"""Check if subscription is suspended: in that case we can't copy mail"""
		try:
			if subscription.suspended_source or subscription.suspended_target:
				if subscription.suspended_source and subscription.suspended_target:
					where = u"both source panel and target panel" 
				elif not subscription.suspended_source and subscription.suspended_target:
					where = u"target panel"
				elif subscription.suspended_source and not subscription.suspended_target:
					where = u"source panel"
				
				issues.append(
					Issue(
						Problem(
							'',
							Problem.WARNING,
							u"The subscription is suspended on %s. "
							u"The migration tool is not able to migrate mail content of suspended subscriptions as POP3/IMAP services are disabled for such subscriptions." % (where,),
						),
						solution=u"To migrate mail content of the subscription:\n"
							u"1) Activate it on both source panel and target PPA.\n"
							u"2) Run 'copy-mail-content' migration tool's command for the subscription.\n"
							u"3) Suspend it on both source panel and target PPA if necessary.\n",
					)
				)
				return False
		except Exception as e:
			logger.error(
				u"Failed to check whether the subscription %s is suspended: %s. The migration tool will proceed with copying mail content even if the subscription is suspended.", 
				subscription.name, e
			)
			logger.debug(u'Exception:', exc_info=e)

		return True

	def _check_domain_mailsystem_disabled(self, domain, issues):
		if not domain.is_enabled:
			issues.append(
				Problem(
					'',
					Problem.WARNING,
					u"Unable to copy mail content on '%s' domain: mail service is disabled due to domain is disabled. " % (domain.name),
				),
				solution=u"To migrate mail content of the mailboxs:\n"
					u"1) Activate domain on both source panel and target PPA.\n"
					u"2) Run 'copy-mail-content' migration tool's command for the subscription.\n"
					u"3) Suspend domain on both source panel and target PPA if necessary.\n",
			)
			return False

		return True

	def _check_mailbox_password(self, 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(u"Mailbox has empty password on E-mail %s, skipping it" % mailbox.full_name)
			return False

		if not mailbox.enabled:
			logger.debug(u"Mailbox is disabled on E-mail %s, skipping it" % mailbox.full_name)
			return False

		if mailbox.password_type != 'plain':
			issues.append(Issue(
				Problem(
					'',
					Problem.WARNING,
					u"Unable to copy mail content: The mailbox password can't be retrieved from Plesk. ",
				),
				solution=u"Change the mailbox password and run the tool with the copy-mail-content command."
			))
			return False

		return True


