import logging
from contextlib import closing, contextmanager
import os.path
from collections import defaultdict

import parallels.common.migrator
from parallels.plesks_migrator.migrator import Migrator as PlesksMigrator
from parallels.common.utils.yaml_utils import write_yaml, read_yaml
from parallels.utils import cached
from parallels.common.context import log_context
from parallels.common.restore_hosting import call_plesk_restore_hosting
from parallels.common import run_command
from parallels.common import MigrationError
from parallels.common.utils import ssh_utils
from parallels.expand_api.operator import PleskDnsOperator, PleskCentralizedDnsZoneOperator
from parallels.utils import find_only, find_first
from .connections import ExpandMigratorConnections
from .converter import Converter, ExpandResellersConverter
from .data_source import DataSource
from parallels.utils import obj, format_list
from migration_list import MigrationList
from parallels.common.checking import PlainReport
from parallels.expand_migrator.workflow import FromExpandWorkflow
from parallels.plesk_api.operator.subscription import Ips

logger = logging.getLogger(__name__)

class Migrator(PlesksMigrator):
	def _load_connections_configuration(self):
		conn = ExpandMigratorConnections(
			self.config, self.target_panel, self._get_migrator_server()
		)
		self.centralized_mail_servers = conn.get_centralized_mail_servers()
		self.centralized_dns_servers = conn.get_centralized_dns_servers()
		self.source_plesks = conn.get_source_plesks()
		self.external_db_servers = conn.get_external_db_servers()
		return conn

	def _create_workflow(self):
		return FromExpandWorkflow()

	def _fetch_expand(self):
		logger.info(u"Fetch Plesk servers information from Expand database.")

		expand_filename = self._get_session_file_path('expand.yaml')
		if not self.global_context.options.reload_source_data and os.path.exists(expand_filename):
			logger.info(u"Expand data file already exists, skip loading")
		else:
			with self._get_data_source() as data_source:
				expand_objects = data_source.get_model(
					self.source_plesks, self.centralized_mail_servers
				)
			self._check_source_data_complete(expand_objects)
			write_yaml(expand_filename, expand_objects)

	def _get_source_servers(self):
		return dict(
			self.conn.get_source_plesks().items() + 
			self.conn.get_centralized_mail_servers().items()
		)

	def _check_source_data_complete(self, expand_objects):
		"""Check that source data (model) is complete.
		   At this moment only check that:
		   - Expand Plesk servers in model include source Plesk servers selected in migrator configuration.
		   - Expand centralized mail servers in model include centralized mail servers selected in migrator configuration.
		"""
		expand_plesk_server_plesk_ids = set([s.plesk_id for s in expand_objects.servers])
		config_plesk_server_plesk_ids = set(self.source_plesks.keys())
		absent_plesk_server_plesk_ids = config_plesk_server_plesk_ids - expand_plesk_server_plesk_ids
		if len(absent_plesk_server_plesk_ids) > 0:
			raise MigrationError(
				u"No Expand Plesk servers are found for source Plesk server(s) %s. " % (format_list(absent_plesk_server_plesk_ids),) + 
				u"Please fix migrator configuration (selected source Plesks, " +
				u"IP addresses in their sections should match IP addresses of the same Plesk servers in Expand)"
			)

		expand_cmail_plesk_ids = set([c.plesk_id for c in expand_objects.centralized_mail_servers])
		config_cmail_plesk_ids = set(self.centralized_mail_servers.keys())
		absent_cmail_plesk_ids = config_cmail_plesk_ids - expand_cmail_plesk_ids
		if len(absent_cmail_plesk_ids) > 0:
			raise MigrationError(
				u"No Expand centralized mail servers are found for configuration section(s) %s. " % (format_list(absent_cmail_plesk_ids),)  + 
				u"Please fix migrator configuration (selected centralized mail servers, " +
				u"IP addresses in their sections should match IP addresses of the same centralized mail servers in Expand)"
			)

	def _get_source_data(self):
		return obj(plesks=self.source_plesks.keys(), expand_objects=read_yaml(self._get_session_file_path('expand.yaml')))

	@classmethod
	def _get_migration_list_class(cls):
		return MigrationList

	def _convert_resellers(self, existing_resellers, resellers_migration_list, report):
		expand_objects = read_yaml(self._get_session_file_path('expand.yaml'))
		return ExpandResellersConverter().convert_resellers(
			expand_objects.expand_resellers, existing_resellers, resellers_migration_list, 
			report, self.global_context.password_holder
		)

	def _convert_accounts(self, converted_resellers, existing_objects, subscriptions_mapping, customers_mapping, report, options):
		expand_objects = read_yaml(self._get_session_file_path('expand.yaml'))
		converter = Converter(self.conn.target.panel_admin_password, existing_objects, expand_objects, options, self.multiple_webspaces)
		# Extract information about customers, subscriptions, regular aux users from hosting Plesk backups
		plain_report = PlainReport(report, *self._extract_source_objects_info())
		mail_servers = {
			plesk_id: self._get_source_servers()[self._get_mail_server_id(plesk_id)]
			for plesk_id in self.source_plesks.iterkeys()
		}
		converter.convert_plesks(self._get_plesk_infos(), plain_report, subscriptions_mapping, customers_mapping, converted_resellers, self.global_context.password_holder, mail_servers)
		# Extract information about mail aux users from centralized mail backups
		# put extacted users under corresponding customer/subscription extracted earlier from hosting Plesks
		converter.extract_aux_users(self._get_plesk_infos(self.centralized_mail_servers), report, self.global_context.password_holder)
		converter.fix_emails_of_aux_users()
		return converter.get_ppa_model()

	@cached
	def _get_centralized_mail_server_by_plesk_server(self):
		result = {}
		expand_objects = read_yaml(self._get_session_file_path('expand.yaml'))
		plesk_server_config_id_by_id = dict((server.id, server.plesk_id) for server in expand_objects.servers)
		for cmail_server in expand_objects.centralized_mail_servers:
			for server_id in cmail_server.assigned_server_ids:
				if server_id in plesk_server_config_id_by_id:
					result[plesk_server_config_id_by_id[server_id]] = cmail_server.plesk_id

		return result

	def _is_mail_centralized(self, plesk_id):
		return plesk_id in self._get_centralized_mail_server_by_plesk_server()

	def _get_mail_plesks_settings(self):
		return dict(self.source_plesks.items() + self.centralized_mail_servers.items())

	def _restore_hosting(self, options, subscription_target_services, finalize):
		logger.info(u"Restore hosting settings of centralized mail")

		self._read_migration_list_lazy(options)
		safe = self._get_safe_lazy()
		ppa_model = self._load_ppa_model()

		subscription_by_name = {subscription.name: subscription for subscription in self._load_ppa_model().iter_all_subscriptions()}
		subscription_target_services = self._get_subscription_target_services() 

		with self.conn.target.main_node_runner() as main_node_runner:
			for plesk_settings in self.centralized_mail_servers.itervalues():
				with log_context(plesk_settings.id), \
					closing(self.load_converted_plesk_backup(plesk_settings)) as backup:
						cmail_subscriptions_to_restore = (
							set(s.name for s in backup.iter_all_subscriptions())	# subscriptions that we have in cmail
							& set(s.name for s in ppa_model.iter_all_subscriptions()) # subscriptions we would like to restore
						)
						disable_apsmail_provisioning = {
							subscription: self._is_mail_assimilated_subscription(subscription_by_name[subscription], subscription_target_services)
							for subscription in cmail_subscriptions_to_restore
						}

						backup_path = self.get_path_to_converted_plesk_backup(plesk_settings.id)
						call_plesk_restore_hosting(
							self.conn.target, backup, backup_path,
							main_node_runner,
							cmail_subscriptions_to_restore, safe,
							disable_apsmail_provisioning=disable_apsmail_provisioning,
							target_backup_path=self.conn.target.main_node_session_file_path('plesk.backup')
						)

						self._restore_catch_all_smartermail_assimilate(disable_apsmail_provisioning, backup)

		super(Migrator, self)._restore_hosting(options, subscription_target_services, finalize)

	def _get_mail_server_id(self, server_id):
		cmail_server = self._get_centralized_mail_server_by_plesk_server().get(server_id)
		if cmail_server is not None:
			return cmail_server
		else:
			return server_id

	def _map_dns_servers(self, plesk_ids, only_slave_servers=False):
		"""Have list of plesk_ids

		   Goal is to return the map so that receiver will distinguish between three situations:
		   1) given plesk_id is not associated with any centralized dns => skip
		   2) given plesk_id is associated with centralized dns but some of these dns servers are not listed in config => fail
		   3) given plesk_id is associated with centralized dns and all centralized dns servers are listed in config => process

		   Then, need to map each plesk_id to corresponding list of dns_server_ips
		   (and receiver will check itself, whether each dns server is listed in config)
		"""
		
		sql = u"""SELECT ps.ip_address AS plesk_server_ip_address, INET_NTOA(ds.ip) AS dns_server_ip_address, dsr.dns_mode as dns_mode
				FROM plesk_server ps
				JOIN dns_server_repository dsr ON dsr.server_id = ps.id
				JOIN dns_slave_server_repository dssr ON dssr.dns_id = dsr.id
				JOIN dns_server ds ON ds.id = dssr.slave_dns_id
			UNION SELECT ps.ip_address AS plesk_server_ip_address, INET_NTOA(ds.ip) AS dns_server_ip_address, dsr.dns_mode as dns_mode
				FROM plesk_server ps
				JOIN dns_server_repository dsr ON dsr.server_id = ps.id
				JOIN dns_server ds ON ds.id = dsr.id"""
		plesk_ip_to_dns_ips = defaultdict(list)
		with self._get_data_source() as ds:
			for row in ds._fetch_db(sql):
				if row['dns_mode'] != 'slave' and only_slave_servers:
					continue
				plesk_ip_to_dns_ips[row['plesk_server_ip_address']].append(row['dns_server_ip_address'])

		return { plesk_id: plesk_ip_to_dns_ips[self.source_plesks[plesk_id].ip] for plesk_id in plesk_ids }

	def _get_source_dns_ips(self, plesk_id):
		ips = super(Migrator, self)._get_source_dns_ips(plesk_id)
		expand_dns_ips = self._map_dns_servers([plesk_id])
		ips += expand_dns_ips[plesk_id]
		return ips

	def _add_slave_subdomain_zones(self, subdomains_masters, plesk_id):
                if len(subdomains_masters) == 0:
                        return set()    # no subdomains - nothing to process and no errors

		err_zones = {}
		try:
			dns_servers_map = self._map_dns_servers(self.source_plesks.keys())
			if plesk_id not in dns_servers_map:
				pass	# given plesk_id is not connected with any centralized DNS, skip Expand DNS forwarding for all its subdomains
			else:
				dns_server_ip_id = {s.ip: server_id for server_id, s in self.centralized_dns_servers.iteritems()}
				for dns_server_ip in dns_servers_map[plesk_id]:
					if dns_server_ip in dns_server_ip_id:
						job = u"\n".join(u"%s %s" % (subdomain.encode('idna'), master) for subdomain, master in subdomains_masters.iteritems()) + '\n'
						dns_server_id = dns_server_ip_id[dns_server_ip]
						with self._get_centralized_dns_runner(dns_server_id) as runner:
							script_filename = 'add_slave_subdomain_zones.sh'
							source_temp_filepath="/tmp/%s" % (script_filename,)
							runner.upload_file(self._get_expand_script_local_path(script_filename), source_temp_filepath)
							runner.run('chmod', ['+x', source_temp_filepath])
							runner.run(source_temp_filepath, stdin_content=job)

							logger.debug("Force retransfer zone data from target server DNS. Serial and TTLs are ignored, zone data is updated always.")
							for subdomain in subdomains_masters.iterkeys():
								runner.sh("rndc retransfer {zone}", dict(zone=subdomain.encode('idna')))
					else:
						logger.error(u"Unable to set up the DNS forwarding for all subdomains of Plesk '%s', because associated centralized DNS server (IP: %s) is not described in migration tool's configuration file", plesk_id, dns_server_ip)
						for zone in subdomains_masters:
							errmsg = u"Unable to set up the DNS forwarding for subdomain '%s' because Expand centralized DNS server (IP: %s) serving its DNS zone is not described in migration tool's configuration file" % (zone, dns_server_ip)
							err_zones[zone] = errmsg
		except Exception as e:
			logger.debug(u"Exception:", exc_info=e)
			logger.error(u"Could not set up the DNS forwarding for all subdomains of Plesk '%s'. Error is: %s", plesk_id, e)
			for zone in subdomains_masters:
				errmsg = u"Could not set up the DNS forwarding for subdomain '%s', error occured on Expand centralized DNS server: '%s'" % (zone, e)
				err_zones[zone] = errmsg

		err_zones.update(super(Migrator, self)._add_slave_subdomain_zones(subdomains_masters, plesk_id))

		return err_zones

	def _remove_slave_subdomain_zones(self, subdomains, plesk_id):
                if len(subdomains) == 0:
                        return set()

		err_zones = {}
		try:
			dns_servers_map = self._map_dns_servers(self.source_plesks.keys())
			if plesk_id not in dns_servers_map:
				pass	# given plesk_id is not connected with any centralized DNS, skip Expand DNS forwarding for all its subdomains
			else:
				dns_server_ip_id = {s.ip: server_id for server_id, s in self.centralized_dns_servers.iteritems()}
				for dns_server_ip in dns_servers_map[plesk_id]:
					if dns_server_ip in dns_server_ip_id:
						job = u"\n".join([subdomain.encode('idna') for subdomain in subdomains]) + '\n'
						dns_server_id = dns_server_ip_id[dns_server_ip]
						with self._get_centralized_dns_runner(dns_server_id) as runner:
							script_filename = 'remove_slave_subdomain_zones.sh'
							source_temp_filepath="/tmp/%s" % (script_filename,)
							runner.upload_file(self._get_expand_script_local_path(script_filename), source_temp_filepath)
							runner.run('chmod', ['+x', source_temp_filepath])
							runner.run(source_temp_filepath, stdin_content=job)
					else:
						logger.error(u"Unable to undo the DNS forwarding for all subdomains of Plesk '%s', because associated centralized DNS server (IP: %s) is not described in migration tool's configuration file", plesk_id, dns_server_ip)
						for zone in subdomains:
							errmsg = u"Unable to undo the DNS forwarding for subdomain '%s' because Expand centralized DNS server (IP: %s) serving its DNS zone is not described in migration tool's configuration file" % (zone, dns_server_ip)
							err_zones[zone] = errmsg
		except Exception as e:
			logger.debug(u"Exception:", exc_info=e)
			logger.error(u"Could not undo the DNS forwarding for all subdomains of Plesk '%s'. Error is: %s", plesk_id, e)
			for zone in subdomains:
				# TODO put DNS server's address into the message
				errmsg = u"Could not undo the DNS forwarding for subdomain '%s', error occured on Expand centralized DNS server: '%s'" % (zone, e)
				err_zones[zone] = errmsg

		err_zones.update(super(Migrator, self)._remove_slave_subdomain_zones(subdomains, plesk_id))

		return err_zones

	def _switch_domains_dns_to_slave(self, domains_info, domains_masters, plesk_id):
		"""When Expand centralized DNS is serving regular (master) Plesk zones in slave mode, 
		   it will not re-transfer them after DNS forwarding is set, because PPA has lower serial numbers.
		   So it takes to make Expand CDNS retransfer these zones once.

		   Plesk migrator should set up the DNS forwarding on Plesk servers prior to updating Expand CDNS.
		"""

		err_zones = super(Migrator, self)._switch_domains_dns_to_slave(domains_info, domains_masters, plesk_id)
		err_zones.update(self._change_cdns_slave_zones(plesk_id, domains_masters, u"set up the DNS forwarding"))
		return err_zones

	def _change_cdns_slave_zones(self, plesk_id, domains_masters, operation_name):

		if len(domains_masters) == 0:
			return set()    # no domains - nothing to process and no errors

		logger.debug("Refresh Expand zones database from Plesk zones database")
		expand_objects = read_yaml(self._get_session_file_path('expand.yaml'))
		expand_server_id = find_only(expand_objects.servers, lambda x: x.plesk_id == plesk_id, "Failed to find Expand server matching '%s' source ID" % (plesk_id,)).id

		self.conn.expand.api().send(
			PleskDnsOperator.Refresh(
				filter=PleskDnsOperator.Refresh.FilterByServerId(expand_server_id)
			)
		)

		logger.debug("Update each DNS zone on a system")

		err_zones = {}
		try:
			dns_servers_map = self._map_dns_servers(self.source_plesks.keys(), only_slave_servers=True)
			if plesk_id not in dns_servers_map:
				pass	# Expand CDNS does not serve zones of this Plesk, skip changing zones
			else:
				dns_server_ip_id = {s.ip: server_id for server_id, s in self.centralized_dns_servers.iteritems()}
				for dns_server_ip in dns_servers_map[plesk_id]:
					if dns_server_ip in dns_server_ip_id:
						dns_server_id = dns_server_ip_id[dns_server_ip]
						with self._get_centralized_dns_runner(dns_server_id) as runner:
							for domain, _ in domains_masters.iteritems():
								try:
									logger.debug("Get centralized DNS zone information for domain '%s'", domain)
									expand_domain = find_first(expand_objects.plesk_domains, lambda x: x.name == domain)
									if expand_domain is not None:
										expand_cdns_zone_id = self.conn.expand.api().send(
											PleskCentralizedDnsZoneOperator.GetZone(
												filter=PleskCentralizedDnsZoneOperator.GetZone.FilterByDomainId([expand_domain.id])
											)
										)[0].data.id

										logger.debug("Syncronize zone from Expand database to CDNS server")
										[result.data for result in self.conn.expand.api().send(
											PleskCentralizedDnsZoneOperator.SyncZone(
												filter=PleskCentralizedDnsZoneOperator.SyncZone.FilterByCdnsZoneId([expand_cdns_zone_id]),
												force=True
											)
										)]
										logger.debug("Force retransfer zone data from target server DNS. Serial and TTLs are ignored, zone data is updated always.")
										runner.sh("rndc retransfer {zone}", dict(zone=domain.encode('idna')))
									else:
										# Aliases configured with domains
										expand_domain_alias = find_first(expand_objects.plesk_domain_aliases, lambda x: x.name == domain)
										if expand_domain_alias is None:
											raise Exception("Failed to find Expand domain '%s'" % domain)
								except Exception as e:
									logger.debug(u"Exception:", exc_info=e)
									errmsg =u"Failed to %s for domain '%s'. Error is: %s" % (operation_name, domain, e)
									logger.error(errmsg)
									err_zones[domain] = errmsg
					else:
						logger.error(u"Failed to %s for all domains of Plesk '%s', because associated centralized DNS server (IP: %s) is not listed in migration tool's configuration file", operation_name, plesk_id, dns_server_ip)
						for zone in domains_masters:
							errmsg = u"Unable to %s for domain '%s', because Expand centralized DNS server serving it (IP: %s) is not described in migration tool's configuration file" % (operation_name, zone, dns_server_ip)
							err_zones[zone] = errmsg
		except Exception as e:
			logger.debug(u"Exception:", exc_info=e)
			logger.error(u"Failed to %s for all domains of Plesk '%s'. Error is: %s", operation_name, plesk_id, e)
			for zone in domains_masters:
				errmsg = u"Could not %s for domain '%s', error occured on Expand centralized DNS server: '%s'" % (operation_name, zone, e)
				err_zones[zone] = errmsg

		return err_zones

	@contextmanager
	def _get_centralized_dns_runner(self, dns_server_id):
		settings = self.centralized_dns_servers[dns_server_id]
		with ssh_utils.connect(settings) as ssh:
			yield run_command.SSHRunner(ssh, "source centralized DNS server '%s' ('%s')" % (dns_server_id, settings.ip))

	def _switch_domains_dns_to_master(self, domains_info, domains_masters, plesk_id):
		"""Restore Expand CDNS <-> Plesk DNS connection for the Plesk zones hosted in CDNS in slave mode.
		   Plesk migrator should undo the DNS forwarding on Plesk servers prior to undoing Expand CDNS.
		"""

		err_zones = super(Migrator, self)._switch_domains_dns_to_master(domains_info, domains_masters, plesk_id)
		err_zones.update(self._change_cdns_slave_zones(plesk_id, domains_masters, u"undo the DNS forwarding"))

		return err_zones

	@contextmanager
	def _get_data_source(self):
		with self.conn.get_expand_runner() as expand_runner:
			yield DataSource(self.conn.expand.api(), expand_runner)

	def _get_expand_script_local_path(self, path):
		dirs = [p for p in parallels.expand_migrator.__path__]
		assert all(d == dirs[0] for d in dirs)
		root = dirs[0]

		return os.path.join(root, 'scripts', path)

	@staticmethod
	def is_expand_mode():
		return True

	def _get_mailserver_ip_by_subscription_name(
			self, mailserver_settings, subscription_name):
		"""Get Expand mail server, which, in fact, does not depend on subsciption.

		Expand mail servers cannot have IPv6 addresses.

		Arguments:
		source_settings: Source server parameters
		subscription name: subscription name, ignored
		"""
		ipv4 = mailserver_settings.ip
		ipv6 = None
		return Ips(ipv4, ipv6) 
