import logging
import os

from collections import defaultdict
from contextlib import contextmanager

from parallels.utils import obj, cached
from parallels.plesks_migrator.infrastructure_checks import checks as infrastructure_checks
from parallels.plesks_migrator.infrastructure_checks.checks import check_windows_copy_content_rsync_connection
from parallels.common.windows_rsync import windows_rsync
from parallels.common.windows_rsync import RsyncInstallerPAExec
from parallels.common.content.mail.psamailbackup import CopyMailPsaMailBackup
from parallels.common.content.mail.imapsync import CopyMailImapsync
from parallels.common.utils.migrator_utils import get_package_extras_file_path
from parallels.common.utils.migrator_utils import get_package_scripts_file_path
from parallels.common.target_panels import TargetPanels
from parallels.common.utils import subscription_filter

from parallels.common.migrator import Migrator as CommonMigrator
from parallels.plesk_api.operator.subscription import Ips
from parallels.helm3_migrator import helm_constants
from parallels.helm3_migrator.helm3_agent import Helm3Agent
from parallels.helm3_migrator.helm3_agent import NoServiceIpHelmDatabase
from parallels.helm3_migrator.global_context import Helm3GlobalMigrationContext
from parallels.helm3_migrator.session_files import Helm3SessionFiles

from .backup_agent import BackupAgent

import connections
import parallels
import parallels.plesks_migrator

from parallels.helm3_migrator.workflow import FromHelm3Workflow

class Migrator(CommonMigrator):
	logger = logging.getLogger(__name__)

	def _load_connections_configuration(self):
		return connections.Helm3MigratorConnections(self.config, self.target_panel, self._get_migrator_server())

	def _create_workflow(self):
		return FromHelm3Workflow()

	def _create_global_context(self):
		context = Helm3GlobalMigrationContext()
		context.helm3_agent = Helm3Agent(self.conn.helm3.get_main_source_server())
		context.source_has_dns_forwarding = False
		return context

	def _create_session_files(self):
		return Helm3SessionFiles(self.conn, self._get_migrator_server())

	# ======================== fetch data from source panel ===================

	def _fetch_data(self, options, overwrite):
		self.logger.info(u"Fetch Helm servers information.")
		pre_backup_filename = self.global_context.session_files.get_path_to_pre_plesk_backup('helm3')

		if not overwrite and os.path.exists(pre_backup_filename):
			self.logger.info(u"Data files for Helm already exist, skip loading")
		else:
			backup_agent = BackupAgent(self.conn.helm3.get_main_source_server())
			backup_agent.make_backup()
			backup_agent.save_backup(pre_backup_filename)

	# ======================== copy web content ===============================

	def _get_subscription_content_ip(self, sub):
		if self._is_fake_domain(sub.name):
			backup = self.load_raw_plesk_backup(self.source_plesks['helm3'])
			domain_name = backup.iter_addon_domains(sub.name).next().name
			return self.get_domain_content_ip(domain_name)

		return self.get_domain_content_ip(sub.name)

	def get_domain_content_ip(self, domain_name):
		try:
			return self._get_service_ip(
				helm_constants.WEB_SERVICE_TYPE, domain_name, 'web'
			)
		except NoServiceIpHelmDatabase:
			self.logger.debug(
				"Subscription has no web IP, try to get FTP IP"
			)
			return self._get_service_ip(
				helm_constants.FTP_SERVICE_TYPE, domain_name, 'ftp'
			)

	def _get_source_web_node(self, subscription_name):
		subscription = self._create_migrated_subscription(subscription_name)
		source_ip = self._get_subscription_content_ip(subscription.raw_backup)
		return self.conn.helm3.get_source_server_by_ip(source_ip)

	# ======================== databases =====================================

	# To dump mysql database need run mysqldump
	def _get_path_to_mysqldump_on_source_server(self, runner_source):
		source_server = self._get_source_node('helm3')
		mysqldump_path = source_server.get_session_file_path('mysqldump.exe')
		extras_mysqldump_path = get_package_extras_file_path(
			parallels.helm3_migrator, "mysqldump.exe"
		)
		runner_source.upload_file(extras_mysqldump_path, mysqldump_path)

		return mysqldump_path

	def _get_path_to_mysql_on_source_server(self, runner_source):
		source_server = self._get_source_node('helm3')
		mysql_path = source_server.get_session_file_path('mysql.exe')
		extras_mysql_path = get_package_extras_file_path(
			parallels.helm3_migrator, "mysql.exe"
		)
		runner_source.upload_file(extras_mysql_path, mysql_path)

		return mysql_path

	# ======================== copy mail content ==============================

	def _copy_mail_content_single_subscription(self, migrator_server, subscription, issues):
		"""Copy mail content of a single subscription"""
		if self.target_panel == TargetPanels.PLESK:
			# For Plesk - use fast PSAMailBackup able to work with Plesk mail
			# servers
			copy_mail = CopyMailPsaMailBackup(
				self._get_mail_provider_by_subscription_name,
				self._windows_rsync
			)
		else:
			# For PPA - use slow imapsync which able to work with practically
			# any kind of mail server, and could transfer mail from Windows
			# mail server to Unix mail server
			copy_mail = CopyMailImapsync(imap_supported=True)

		copy_mail.copy_mail(migrator_server, subscription, issues)

	def _get_mail_provider_by_subscription_name(self, name):
		if self._is_fake_domain(name):
			backup = self.load_raw_plesk_backup(self.source_plesks['helm3'])
			domain_name = backup.iter_addon_domains(name).next().name
			return self.global_context.helm3_agent.get_mail_provider_by_subscription_name(domain_name)

		return self.global_context.helm3_agent.get_mail_provider_by_subscription_name(name)

	def _get_mailserver_ip_by_subscription_name(self, source_settings, subscription_name):
		"""Get IP address of source mailserver for subscription"""
		domain_name = self._get_first_real_domain_with_mail(subscription_name)
		if domain_name is None:
			return Ips(None, None)
		ipv4 = self._get_service_ip(helm_constants.MAIL_SERVICE_TYPE, domain_name, 'mail')
		return Ips(ipv4, None)

	def _get_first_real_domain_with_mail(self, subscription_name):
		"""Get first real (not fake) domain of subscription with mail"""
		subscription = self._create_migrated_subscription(
			subscription_name
		)
		if subscription.is_fake:
			for site in subscription.raw_backup.iter_sites():
				if site.mailsystem is not None and site.mailsystem.enabled:
					return site.name
		else:
			mailsystem = subscription.raw_backup.mailsystem
			if mailsystem is not None and mailsystem.enabled:
				return subscription.name

		# None of domains has enabled mailsystem
		return None

	def _get_source_mail_node(self, subscription_name):
		"""Get source mail server for subscription (SourceServer object)""" 
		node_ips = self._get_mailserver_ip_by_subscription_name(None, subscription_name)
		if node_ips.v4 is not None:
			return self.conn.helm3.get_source_server_by_ip(node_ips.v4)
		else:
			return None

	# ======================== infrastructure checks ==========================

	def _check_infrastructure_connections(self, report, safe):
		"""Check infrastructure - connections and disk space requirements"""

		self.logger.info(u"Check connection requirements")
		checks = infrastructure_checks.InfrastructureChecks()

		web_report = report.subtarget(u"Connections between source and the destination web server nodes", None)
		with safe(web_report, "Failed to check connections between source and the destination web server nodes"):
			self._check_windows_copy_web_content_rsync_connections(checks, web_report)

		mail_report = report.subtarget(u"Connections between source and the destination mail server nodes", None)
		with safe(mail_report, "Failed to check connections between source and the destination mail server nodes"):
			self._check_copy_mail_content(checks, mail_report)

		db_report = report.subtarget(u"Connections between source and the destination database server nodes", None)
		with safe(db_report, "Failed to check connections between source and the destination database server nodes"):
			self._check_windows_copy_db_content_rsync_connections(checks, db_report)
			self._check_windows_copy_mssql_db_content(checks, db_report)

	def _check_disk_space(self, report, safe):
		"""Check disk space requirements for source and target servers"""

		self.logger.info(u"Check disk space requirements")
		disk_space_report = report.subtarget(u"Disk space requirements", None)
		self._check_disk_space_windows(disk_space_report)

	def _check_copy_mail_content(self, checks, report):
		"""Check connections necessary to copy mail content"""

		self.logger.info("Check connections necessary to copy mail content")
		
		if self._use_psmailbackup():
			self._check_copy_mail_content_psamailbackup(checks, report)
		else:
			self._check_copy_mail_content_imap_pop3(checks, report)

	def _check_copy_mail_content_imap_pop3(self, checks, report):
		"""Check connections necessary to copy mail by IMAP/POP3"""

		check_source_imap = defaultdict(list)
		check_source_pop3 = defaultdict(list)
		check_target_imap = defaultdict(list)

		for subscription in filter(
			subscription_filter.copy_mail_full, 
			self._iter_all_subscriptions()
		):
			check_source_imap[subscription.mail_source_server].append(
				subscription.name
			)
			check_target_imap[subscription.mail_target_server].append(
				subscription.name
			)

		for subscription in filter(
			subscription_filter.copy_mail_messages, 
			self._iter_all_subscriptions()
		):
			check_source_pop3[subscription.mail_source_server].append(
				subscription.name
			)

		checks.check_windows_mail_source_connection(
			subscriptions_by_source=check_source_imap,
			report=report,
			checker=infrastructure_checks.SourceNodeImapChecker()
		)
		checks.check_windows_mail_source_connection(
			subscriptions_by_source=check_source_pop3,
			report=report,
			checker=infrastructure_checks.SourceNodePop3Checker()
		)
		checks.check_windows_mail_target_connection(
			subscriptions_by_target=check_target_imap,
			report=report,
			checker=infrastructure_checks.TargetNodeImapChecker()
		)

	def _check_copy_mail_content_psamailbackup(self, checks, report):
		"""Check connections necessary to copy mail with PSAMailBackup"""

		check_rsync = []

		for subscription in filter(
			subscription_filter.copy_mail, 
			self._iter_all_subscriptions()
		):
			target_server = subscription.mail_target_server
			source_server = subscription.mail_source_server
			check_rsync.append(
					infrastructure_checks.SubscriptionsInfo(
						subscription_name=subscription.name,
						subscription_dir=self._get_first_real_domain_with_mail(
							subscription.name
						),
						target_node=target_server,
						source_ip=source_server.ip()
				)
			)

		check_windows_copy_content_rsync_connection(
			check_rsync, self._windows_rsync, "mail", report
		)

	def _check_disk_space_windows(self, report):
		self.logger.info("Calculating disk space usage on the source windows servers")

		web_diskspace_usages = []
		mssql_diskspace_usages = []
		mysql_diskspace_usages = []

		subscription_target_services = self._get_subscription_target_services_cached()
		for subscription_name in subscription_target_services:
			if self._is_fake_domain(subscription_name):
				continue

			target_nodes=self._get_subscription_nodes(subscription_name)
			web_diskspace_usages.append(
				obj(
					usage_source=self.global_context.helm3_agent.get_domain_diskspace_usage(
						subscription_name,
						helm_constants.WEB_SERVICE_TYPE
					),
					subscription_name=subscription_name,
					target_node=target_nodes.web
				)
			)

		for db in self._list_databases_to_copy(subscription_target_services):
			if db.src_db_server.dbtype == 'mysql' and db.target_node.is_windows():
				mysql_diskspace_usages.append(
					obj(
						usage_source=self.global_context.helm3_agent.get_domain_diskspace_usage(
							db.subscription_name,
							helm_constants.MYSQL_SERVICE_TYPE
						),
						subscription_name=db.subscription_name,
						target_node=db.target_node.get_subscription_node(),
						db = obj(
							db_name=db.db_name,
							db_target_node=db.target_node
						)
					)
				)
			if db.src_db_server.dbtype == 'mssql':
				mssql_diskspace_usages.append(
					obj(
						usage_source=self.global_context.helm3_agent.get_domain_diskspace_usage(
							db.subscription_name,
							helm_constants.MSSQL_SERVICE_TYPE
						),
						subscription_name=db.subscription_name,
						target_node=db.target_node.get_subscription_node(),
						db = obj(
							db_name=db.db_name,
							db_target_node=db.target_node
						)
					)
				)

		self.logger.info("Checking if there is enough disk space on Plesk")
		disk_usage_script_name = 'folder_disk_usage.vbs'
		mssql_disk_usage_script = 'mssql_disk_usage.ps1'
		checker_target = infrastructure_checks.WindowsDiskSpaceChecker()
		diskspace_usages = web_diskspace_usages + mysql_diskspace_usages + mssql_diskspace_usages
		for target_node in set(
				subs_info.target_node for subs_info in diskspace_usages
		):
			subscription_names = [subs_info.subscription_name for subs_info in diskspace_usages if subs_info.target_node == target_node]
			usages_source_web = sum([subs_info.usage_source for subs_info in web_diskspace_usages if subs_info.target_node == target_node])
			usages_source_mysql = defaultdict(list)      # { db_server: [ usage ] }
			for mysql_diskspace_usage in mysql_diskspace_usages:
				if mysql_diskspace_usage.target_node == target_node:
					usages_source_mysql[(
						mysql_diskspace_usage.db.db_target_node.host(),
						mysql_diskspace_usage.db.db_target_node.port()
					)].append(mysql_diskspace_usage.usage_source)
			usages_source_mssql = defaultdict(list)      # { db_server: [ usage ] }
			for mssql_diskspace_usage in mssql_diskspace_usages:
				if mssql_diskspace_usage.target_node == target_node:
					usages_source_mssql[mssql_diskspace_usage.db.db_target_node.host()].append(mssql_diskspace_usage.usage_source)
			with target_node.runner() as runner_target:
				checker_target.check_with_source_usages(
					target_node,
					mysql_bin=self._get_windows_mysql_client(runner_target),
					du_local_script_path=get_package_scripts_file_path(
						parallels.plesks_migrator, disk_usage_script_name
					),
					mssql_disk_usage_local_script_path=get_package_scripts_file_path(
						parallels.plesks_migrator, mssql_disk_usage_script
					),
					usage_source_web=usages_source_web,
					usages_source_mysql_db=usages_source_mysql,
					usages_source_mssql_db=usages_source_mssql,
					domains=subscription_names,
					mysql_databases=[subs_info.db for subs_info in mysql_diskspace_usages if subs_info.target_node == target_node],
					mssql_databases=[subs_info.db for subs_info in mssql_diskspace_usages if subs_info.target_node == target_node],
					report=report
				)

	# ======================== utility functions ==============================

	@cached
	def _is_fake_domain(self, subscription_name):
		"""Detect if subscription is fake (so it does not exist on source)

		 In case when client has one domain with enabled web or ftp we don't
		 create fake domain. In all other cases we create fake domain
		"""
		return self.global_context.helm3_agent.is_fake_domain(subscription_name)

	def _get_service_ip(self, service_type_id, domain_name, content_type):
		# Command returns all IP addresses of a server where specified service of domain is located
		service_ips = self.global_context.helm3_agent.get_service_ips(domain_name, service_type_id)
			
		for server in self.conn.helm3.source_servers:
			if server.ip() in service_ips:
				return server.ip()
		raise NoServiceIpHelmDatabase(
			"Can't get Helm node IP with {content_type} content for domain '{domain_name}'. "
			"Migrator will skip copy {content_type} content for this domain. "
			"Most probably that is an inconsistency in the source panel database. "
			"Resolve it to copy {content_type} content for this domain.".format(
				content_type=content_type, domain_name=domain_name
			)
		)

	@contextmanager
	def _windows_rsync(self, source_server, target_server, source_ip=None):
		if source_server is None:
			source_server = self.conn.helm3.get_source_server_by_ip(source_ip)
		source_vhosts_dir = self.global_context.helm3_agent.get_vhosts_dir_source(
			source_server
		)

		if self.conn.target.is_local:
			rsync_installer_source = RsyncInstallerPAExec(
				self._get_migrator_server(), source_server
			)
		else:
			rsync_installer_source = None # use defaults

		with windows_rsync(
			source_server, target_server, source_vhosts_dir,
			rsync_installer_source=rsync_installer_source
		) as rsync:
			yield rsync


