from collections import defaultdict
from parallels.common.utils import windows_utils
from parallels.common.migrator_config import MailContent
from parallels.plesks_migrator.infrastructure_checks.checks import \
		NodesPair, \
		DiskSpaceDatabase, \
		DiskSpaceDatabaseAccess, \
		MSSQLConnectionData 

class InfrastructureCheckLister(object):
	"""Group subscriptions by various criteria.
	
	List different information about subscriptions and databases 
	grouped in various ways to perform different infrastructure checks"""

	def __init__(self, data_source):
		"""
		data_source - instance of InfrastructureCheckListerDataSource
		"""
		self.data_source = data_source

	# ======================== public functions ========================

	def get_subscriptions_by_web_nodes(self):
		"""List subscriptions grouped by source and target web service node.
		
		Returns: dictionary {
			NodesPair(source web node, target web node)
				[list of subscriptions]
		}"""
		return self._get_subscriptions_by_web_nodes(is_windows=None)

	def get_subscriptions_to_target_web_node(self):
		"""List subscriptions to target web node.
		
		Returns: dictionary {
			subscription : target web node
		}"""
		return {
			subscription: nodes.target
			for nodes, subscriptions
			in self.get_subscriptions_by_web_nodes().iteritems()
			for subscription in subscriptions
		}

	def get_subscriptions_by_unix_web_nodes(self):
		"""List subscriptions that have web service on Unix nodes,
		grouped by source and target web service node.
		
		Returns: dictionary {
			NodesPair(source Unix web node, target Unix web node)
				[list of subscriptions]
		}"""
		return self._get_subscriptions_by_web_nodes(is_windows=False)

	def get_subscriptions_by_windows_web_nodes(self):
		"""List subscriptions that have web service on Windows nodes,
		grouped by source and target web nodes.
		
		Returns: dictionary {
			NodesPair(source Windows web node, target Windows web node)
				[list of subscriptions]
		}"""
		return self._get_subscriptions_by_web_nodes(is_windows=True)

	def get_subscriptions_by_unix_mail_nodes(self):
		"""List subscriptions that have mail service on Unix nodes,
		grouped by source and target mail nodes.

		For such nodes we copy content by rsync, in all the other
		cases we use IMAP/POP3.
		
		Returns: dictionary {
			NodesPair(source Unix mail node, target Unix mail node): 
				[list of subscriptions]
		} 
		"""
		return self._group_subscriptions_by_nodes(
			self._get_subscription_to_source_mail_node(is_windows=False),
			self._get_subscription_to_target_mail_node(is_windows=False)
		)

	def get_subscriptions_by_mail_nodes(self):
		"""List subscriptions grouped by source and target mail nodes.

		Returns: dictionary {
			NodesPair(source mail node, target mail node): 
				[list of subscriptions]
		} 
		"""
		return self._group_subscriptions_by_nodes(
			self._get_subscription_to_source_mail_node(is_windows=None),
			self._get_subscription_to_target_mail_node(is_windows=None)
		)

	def get_subscriptions_to_target_mail_node(self):
		"""List subscriptions to target mail node.
		
		Returns: dictionary {
			subscription : target mail node
		}"""
		return {
			subscription: nodes.target
			for nodes, subscriptions
			in self.get_subscriptions_by_mail_nodes().iteritems()
			for subscription in subscriptions
		}

	def get_subscriptions_by_unix_db_nodes(self):
		"""List databases (of all types) on Unix nodes, grouped by source and target
		database nodes.

		Returns dictionary {
			NodesPair(source database node, target database node):
				[ 
					list of dictionaries {
						db_name=database name, 
						db_type=database type (mysql/mssql/postgres), 
						subscription=subscription name
					}
				]
		}
		"""
		return self._get_subscriptions_by_db_nodes(is_windows=False)

	def get_subscriptions_by_windows_mysql_nodes(self):
		"""List MySQL databases on Windows nodes, grouped by source and target
		database nodes.

		Returns dictionary {
			NodesPair(source database node, target database node):
				[ 
					list of dictionaries {
						db_name=database name, 
						db_type=database type (always 'mysql')
						subscription=subscription name
					}
				]
		}
		"""
		return self._get_subscriptions_by_db_nodes(is_windows=True, db_type_filter='mysql')

	def get_subscriptions_by_mssql_instances(self):
		"""List MSSQL databases by Windows nodes, grouped by source and target
		database nodes.

		Returns dictionary {
			NodesPair(source database node, target database node):
				[ 
					list of dictionaries {
						db_name=database name, 
						db_type=database type (always 'mssql')
						subscription=subscription name
					}
				]
		}
		"""
		result = defaultdict(list)

		for database_info in self.data_source.list_databases_to_copy():
			source = database_info.source_database_server
			target = database_info.target_database_server
			if source.type() == 'mssql':
				source_mssql_connection_data = self._get_source_mssql_connection_data(database_info)

				nodes_pair = NodesPair(
					source=self.data_source.get_source_node(source.access_settings.id),
					target=target.panel_server
				)
				result[(nodes_pair, source_mssql_connection_data)].append(dict(
					db_name=database_info.database_name,
					db_type=source.type(),
					subscription=database_info.subscription_name,
				))

		return dict(result)

	def get_subscriptions_to_check_source_mail_connections(self, copy_mail_content_mode):
		"""List subscriptions for which we should check connection to the
		source server by IMAP or POP3, depending on copy mail content mode. 
		List subscriptions grouped by source node.
		
		Returns dictionary {
			source node: [list of subscriptions]
		}
		"""
		result = defaultdict(list)

		subscription_to_target = self._get_subscription_to_target_mail_node()
		subscription_to_source = self._get_subscription_to_source_mail_node()
		for subscription_name in set(subscription_to_target) & set(subscription_to_source):
			source_node = subscription_to_source[subscription_name]
			target_node = subscription_to_target[subscription_name]

			if source_node.ip() == target_node.ip():
				# Assimilated mail server - no need to copy content, no need to check
				continue 

			if source_node.is_windows() == False and target_node.is_windows() == False:
				# Unix to Unix copy mode: no need to check mail server connections as rsync is used
				continue 

			if not source_node.mail_settings.mode == copy_mail_content_mode:
				# Another way of copy mail content should be checked
				continue

			result[source_node].append(subscription_name)

		return dict(result)

	def get_subscriptions_to_check_target_imap(self):
		"""List subscriptions for which we should check connection to the
		target server by IMAP. List subscriptions grouped by source node.
		
		Returns dictionary {
			target node: [list of subscriptions]
		}
		"""
		result = defaultdict(list)

		subscription_to_target = self._get_subscription_to_target_mail_node()
		subscription_to_source = self._get_subscription_to_source_mail_node()
		for subscription_name in set(subscription_to_target) & set(subscription_to_source):
			source_node = subscription_to_source[subscription_name]
			target_node = subscription_to_target[subscription_name]

			if source_node.ip() == target_node.ip():
				# Assimilated mail server - no need to copy content, no need to check
				continue 

			if source_node.is_windows() == False and target_node.is_windows() == False:
				# Unix to Unix copy mode: no need to check mail server connections as rsync is used
				continue 

			if not source_node.mail_settings.mode in (MailContent.FULL, MailContent.MESSAGES):
				# We don't copy content from that source node, so no need to check IMAP conection
				continue

			result[target_node].append(subscription_name)

		return dict(result)

	def get_mysql_databases_info_for_disk_space_checker(self, is_windows):
		"""List MySQL databases to perform disk space checks. 
		
		Returns dictionary {
			NodesPair(source database node, target database node):
				[
					list of DiskSpaceDatabase objects
					with DiskSpaceDatabaseAccess as src_access and db_target_node
				]
		}
		"""
		return self._get_databases_info_for_disk_space_checker(
			is_windows=is_windows, dbtype='mysql', 
			convert_db_object_func=self._convert_mysql_db_object
		)

	def get_mssql_databases_info_for_disk_space_checker(self):
		"""List MSSQL databases to perform disk space checks.

		Returns dictionary {
			NodesPair(source database node, target database node):
				[
					list of DiskSpaceDatabase objects
					with MSSQLConnectionData as src_access and db_target_node
				]
		}
		convert_db_object_func is a function that accepts database object
		provided by list_databases_to_copy, and returns object suitable for 
		disk space checks (see convert_mysql_db_object and convert_mssql_db_object 
		functions below)
		"""
		return self._get_databases_info_for_disk_space_checker(
			is_windows=True, dbtype='mssql', 
			convert_db_object_func=self._convert_mssql_db_object
		)

	# ======================== private functions ========================

	def _get_databases_info_for_disk_space_checker(self, is_windows, dbtype, convert_db_object_func):
		"""List databases to perform disk space checks.
		convert_db_object_func is a function that accepts database object
		provided by list_databases_to_copy, and returns object suitable for 
		disk space checks (see _convert_mysql_db_object and _convert_mssql_db_object 
		functions below)
		"""
		result = defaultdict(list)

		for database_info in self.data_source.list_databases_to_copy():
			source = database_info.source_database_server
			target = database_info.target_database_server
			if source.type() == dbtype:
				source_node = source.panel_server
				if source_node.is_windows() == is_windows:
					disk_space_database = convert_db_object_func(database_info)
					result[NodesPair(source_node, target.panel_server)].append(disk_space_database)

		return result

	@staticmethod
	def _convert_mysql_db_object(database_info):
		"""Convert MySQL database object returned by list_databases_to_copy to
		DiskSpaceDatabase, ready to be passed into
		infrastructure checker. Use this function with 
		_get_databases_info_for_disk_space_checker function as convert_db_object_func
		"""
		source = database_info.source_database_server
		target = database_info.target_database_server
		return DiskSpaceDatabase(
			src_access=DiskSpaceDatabaseAccess(
				host=source.host(),
				port=source.port(),
				login=source.user(),
				password=source.password(),
			),
			db_target_node=target,
			db_name=database_info.database_name
		)

	@staticmethod
	def _convert_mssql_db_object(database_info):
		"""Convert MSSQL database object returned by list_databases_to_copy to
		DiskSpaceDatabase, ready to be passed into 
		infrastructure checker. Use this function with 
		_get_databases_info_for_disk_space_checker function as convert_db_object_func
		"""
		source = database_info.source_database_server
		target = database_info.target_database_server
		return DiskSpaceDatabase(
			src_access=MSSQLConnectionData(
				host=windows_utils.get_dbbackup_mssql_host(source.host(), source.ip()),
				login=source.user(),
				password=source.password()
			),
			db_target_node=target,
			db_name=database_info.database_name
		)


	def _get_source_mssql_connection_data(self, database_info):
		source = database_info.source_database_server
		return MSSQLConnectionData(
			host=windows_utils.get_dbbackup_mssql_host(source.host(), source.ip()),
			login=source.user(),
			password=source.password()
		)

	def _get_subscriptions_by_db_nodes(self, is_windows, db_type_filter=None):
		"""Returns: dict with 
			key - tuple (source Plesk id, target database web node POA host id)
			value - list of subscriptions
		"""
		result = defaultdict(list)
		for database_info in self.data_source.list_databases_to_copy():
			source = database_info.source_database_server
			target = database_info.target_database_server
			if (
				# both target and source nodes are of the same platform
				source.is_windows() == is_windows and target.is_windows() == is_windows
				and 
				(db_type_filter is None or source.type() == db_type_filter)
			): 
				source_panel_id = source.access_settings.id
				key = NodesPair(
					source=self.data_source.get_source_node(source_panel_id),
					target=target.panel_server
				)
				result[key].append(dict(
					db_name=database_info.database_name,
					db_type=source.type(),
					subscription=database_info.subscription_name
				))

		return dict(result)

	def _get_subscriptions_by_web_nodes(self, is_windows):
		return self._group_subscriptions_by_nodes(
			subscription_to_source_node=self._get_subscription_to_source_node(is_windows),
			subscription_to_target_node=self._get_subscription_to_target_web_node()
		)

	def _group_subscriptions_by_nodes(self, subscription_to_source_node, subscription_to_target_node):
		"""Group subscriptions by target and source nodes.

		source_subscriptions - dictionary {subscription name: source node}
		target_subscriptions - dictionary {subscription name: target node}
		Result: dictionary {NodesPair(source node, target node): [list of subscriptions]}
		"""
		subscriptions_by_nodes = defaultdict(list)

		for subscription in set(subscription_to_source_node.keys()) & set(subscription_to_target_node.keys()):
			source_node = subscription_to_source_node[subscription]
			target_node = subscription_to_target_node[subscription]
			nodes_pair = NodesPair(source_node, target_node)
			subscriptions_by_nodes[nodes_pair].append(subscription)

		return dict(subscriptions_by_nodes)

	def _get_subscription_to_target_web_node(self):
		"""Get dictionary {subscription name: target web node}"""
		return self._get_subscription_to_target_node(lambda nodes: nodes.web)

	def _get_subscription_to_target_mail_node(self, is_windows=None):
		"""Get dictionary {subscription name: target web node}"""
		return {
			subscription: node
			for subscription, node
			in self._get_subscription_to_target_node(lambda nodes: nodes.mail).iteritems()
			if node.is_windows() == is_windows or is_windows is None
		}

	def _get_subscription_to_source_mail_node(self, is_windows=None):
		target_model = self.data_source.get_target_model()
		subscription_to_node = {
			subscription.name: 
				self.data_source.get_source_mail_node(subscription.name)
			for subscription in target_model.iter_all_subscriptions()
		}

		subscription_to_node_filtered = {}
		for subscription_name, node in subscription_to_node.iteritems():
			if node is None:
				continue
			if node.is_windows() == is_windows or is_windows is None:
				subscription_to_node_filtered[subscription_name] = node

		return subscription_to_node_filtered

	def _get_subscription_to_source_node(self, is_windows):
		"""Get dictionary {subscription name: source node}"""
		return {
			subscription.name: \
				self.data_source.create_migrated_subscription(subscription.name).web_source_server
			for subscription in self.data_source.get_target_model().iter_all_subscriptions()
			if subscription.is_windows == is_windows or is_windows is None
		}

	def _get_subscription_to_target_node(self, node_function):
		"""Get dictionary {subscription name: target node}, where target 
		node is selected from subscriptions nodes with node_function"""
		result = {}
	
		model_subscriptions = set([s.name for s in self.data_source.get_target_model().iter_all_subscriptions()])

		for subscription_name in model_subscriptions:
			node = node_function(self.data_source.get_target_nodes(subscription_name))
			if node is not None:
				result[subscription_name] = node

		return result

class InfrastructureCheckListerDataSource(object):
	"""Get database list or server by subscription name.
	
	Provide necessary information about subscriptions and nodes to
	InfrastructureCheckLister class. This class is abstract, inherit and define
	necessary data sources
	"""

	def list_databases_to_copy(self):
		"""Provide list of databases migration tool is going to copy
		(list of parallels.plesks_migrator.migrator.DatabaseToCopy)"""
		raise NotImplementedError()

	def get_source_node(self, source_node_id):
		"""Get SourceServer object by source node ID"""
		raise NotImplementedError()

	def get_source_mail_node(self, subscription_name):
		"""Get SourceServer object for source mail node 
		by main (web hosting) source node ID. Mail node may differ
		from web hosting node in case of Expand Centralized Mail 
		for example"""
		raise NotImplementedError()

	def get_target_nodes(self, subscription_name):
		"""Get target nodes (SubscriptionNodes) of subscription"""
		raise NotImplementedError()

	def get_target_model(self):
		"""Get target data model (common.target_data_model.Model)"""
		raise NotImplementedError()

	def create_migrated_subscription(self):
		"""Create MigrationSubscription object for subscription"""
		raise NotImplementedError()
