from parallels.plesk.source.expand import messages
import logging

import parallels.core.migrator
from parallels.core.utils.migrator_utils import get_package_scripts_file_path
from parallels.plesk.source.expand.client_subscription_adapter import ExpandToPleskClientSubscriptionConverterAdapter
from parallels.plesk.source.expand.global_context import ExpandGlobalMigrationContext
from parallels.plesk.source.expand.session_files import ExpandSessionFiles
from parallels.plesk.source.plesk.migrator import Migrator as PlesksMigrator
from parallels.core.utils.common import cached, group_value_by
from parallels.plesk.utils.xml_rpc.expand.operator import PleskDnsOperator, PleskCentralizedDnsZoneOperator
from parallels.core.utils.common import find_only, find_first
from .connections import ExpandMigratorConnections
from parallels.plesk.source.expand.converter.reseller import ExpandResellersConverter
from migration_list import MigrationList
from parallels.plesk.utils.xml_rpc.plesk.operator.subscription import Ips


logger = logging.getLogger(__name__)


class Migrator(PlesksMigrator):
	"""
	:type global_context: ExpandGlobalMigrationContext
	"""
	def __init__(self, config):
		self.global_context = None
		super(Migrator, self).__init__(config)

	def _load_connections_configuration(self):
		conn = ExpandMigratorConnections(
			self.global_context, self._get_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_servers = conn.get_source_plesks()
		return conn

	def _create_global_context(self):
		return ExpandGlobalMigrationContext()

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

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

	def _get_migration_list_source_data(self):
		return MigrationListSourceData(
			plesks=self.source_servers.keys(),
			expand_objects=self.global_context.expand_data
		)

	@classmethod
	def _get_migration_list_class(cls):
		return MigrationList

	def _convert_resellers(self, existing_resellers, resellers_migration_list, report):
		return ExpandResellersConverter().convert_resellers(
			self.global_context.expand_data.expand_resellers, existing_resellers, resellers_migration_list,
			report, self.global_context.password_holder
		)

	def _convert_accounts(self, converted_resellers, report):
		converter_adapter = ExpandToPleskClientSubscriptionConverterAdapter()
		return converter_adapter.convert(self.global_context, converted_resellers, report)

	@cached
	def _get_centralized_mail_server_by_plesk_server(self):
		result = {}
		plesk_server_config_id_by_id = {
			server.id: server.plesk_id for server in self.global_context.expand_data.servers
		}
		for cmail_server in self.global_context.expand_data.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_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 _get_source_dns_ips(self, source_id):
		ips = super(Migrator, self)._get_source_dns_ips(source_id)
		expand_dns_ips = [
			mapping.dns_server_ip
			for mapping in self.global_context.expand_data.dns_server_mapping
		]
		ips += expand_dns_ips
		return ips

	def _add_slave_subdomain_zones(self, subdomains_masters, plesk_id):
		if len(subdomains_masters) == 0:
			return set()

		err_zones = {}
		try:
			dns_servers_map = group_value_by(
				self.global_context.expand_data.dns_server_mapping,
				key_func=lambda mapping: mapping.plesk_id,
				value_func=lambda mapping: mapping.dns_server_ip
			)
			if plesk_id not in dns_servers_map:
				# given plesk_id is not connected with any centralized DNS,
				# skip Expand DNS forwarding for all its subdomains
				pass
			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]
						centralized_dns_server = self.conn.expand_centralized_dns_server(dns_server_id)
						with centralized_dns_server.runner() as runner:
							script_filename = 'add_slave_subdomain_zones.sh'
							source_temp_filepath = "/tmp/%s" % (script_filename,)
							runner.upload_file(
								get_package_scripts_file_path(parallels.plesk.source.expand, script_filename),
								source_temp_filepath
							)
							runner.run('chmod', ['+x', source_temp_filepath])
							runner.run(source_temp_filepath, stdin_content=job)

							logger.debug(
								messages.FORCE_RETRANSFER_ZONE_DATA_FROM_TARGET)
							for subdomain in subdomains_masters.iterkeys():
								runner.sh("rndc retransfer {zone}", dict(zone=subdomain.encode('idna')))
					else:
						logger.error(
							messages.UNABLE_TO_SET_DNS_FORWARDING_FOR_ALL_SUBDOMAINS_CDNS_NOT_DESCRIBED, plesk_id, dns_server_ip
						)
						for zone in subdomains_masters:
							errmsg = (
								messages.UNABLE_TO_SET_DNS_FORWARDING_FOR_SUBDOMAIN_CDNS_NOT_DESCRIBED % (
									zone, dns_server_ip
								)
							)
							err_zones[zone] = errmsg
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.error(
				messages.SET_ALL_SUBDOMAINS_DNS_FORWARDING_FAILURE, plesk_id, e
			)
			for zone in subdomains_masters:
				errmsg = (
					messages.SET_SUBDOMAIN_DNS_FORWARDING_FAILURE % (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 = group_value_by(
				self.global_context.expand_data.dns_server_mapping,
				key_func=lambda mapping: mapping.plesk_id,
				value_func=lambda mapping: mapping.dns_server_ip
			)
			if plesk_id not in dns_servers_map:
				# given plesk_id is not connected with any centralized DNS,
				# skip Expand DNS forwarding for all its subdomains
				pass
			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]
						centralized_dns_server = self.conn.expand_centralized_dns_server(dns_server_id)
						with centralized_dns_server.runner() as runner:
							script_filename = 'remove_slave_subdomain_zones.sh'
							source_temp_filepath = "/tmp/%s" % (script_filename,)
							runner.upload_file(
								get_package_scripts_file_path(parallels.plesk.source.expand, script_filename),
								source_temp_filepath
							)
							runner.run('chmod', ['+x', source_temp_filepath])
							runner.run(source_temp_filepath, stdin_content=job)
					else:
						logger.error(
							messages.UNABLED_TO_UNDO_DNS_FORWARDING_FOR_ALL_SUBDOMAINS_CDNS_NOT_DESCRIBED,
							plesk_id, dns_server_ip
						)
						for zone in subdomains:
							errmsg = (
								messages.UNABLE_UNDO_DNS_FORWARDING_FOR_SUBDOMAIN_CDNS_NOT_DESCRIBED % (
									zone, dns_server_ip
								)
							)
							err_zones[zone] = errmsg
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.error(
				messages.UNDO_DNS_FORWARDING_ALL_SUBDOMAINS_FAILURE, plesk_id, e
			)
			for zone in subdomains:
				# TODO put DNS server's address into the message
				errmsg = (
					messages.UNDO_SUBDOMAIN_DNS_FORWARDING_FAILURE % (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 may not re-transfer them after DNS forwarding is set, because target server may have 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(messages.LOG_REFRESH_EXPAND_ZONES_DATABASE)
		expand_server_id = find_only(
			self.global_context.expand_data.servers,
			lambda x: x.plesk_id == plesk_id, messages.FAILED_TO_FIND_EXPAND_SERVER % (plesk_id,)
		).id

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

		logger.debug(messages.LOG_UPDATE_EACH_DNS_ZONE)

		err_zones = {}
		try:
			dns_servers_map = group_value_by(
				[m for m in self.global_context.expand_data.dns_server_mapping if m.mode == 'slave'],
				key_func=lambda mapping: mapping.plesk_id,
				value_func=lambda mapping: mapping.dns_server_ip
			)
			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]
						centralized_dns_server = self.conn.expand_centralized_dns_server(dns_server_id)
						with centralized_dns_server.runner() as runner:
							for domain, _ in domains_masters.iteritems():
								try:
									logger.debug(messages.LOG_GET_CDNS_ZONE_INFO, domain)
									expand_domain = find_first(
										self.global_context.expand_data.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(messages.LOG_SYNC_ZONE_FROM_EXPAND_TO_CDNS)
										for result in self.conn.expand.api().send(
											PleskCentralizedDnsZoneOperator.SyncZone(
												filter=PleskCentralizedDnsZoneOperator.SyncZone.FilterByCdnsZoneId(
													[expand_cdns_zone_id]
												),
												force=True
											)
										):
											result.check()
										logger.debug(
											messages.LOG_FORCE_RETRANSFER_ZONE_DATA_FROM_TARGET)
										runner.sh("rndc retransfer {zone}", dict(zone=domain.encode('idna')))
									else:
										# Aliases configured with domains
										expand_domain_alias = find_first(
											self.global_context.expand_data.plesk_domain_aliases,
											lambda x: x.name == domain
										)
										if expand_domain_alias is None:
											raise Exception(messages.FAILED_TO_FIND_EXPAND_DOMAIN % domain)
								except Exception as e:
									logger.debug(messages.LOG_EXCEPTION, exc_info=e)
									errmsg = messages.FAILED_TO_PERFORM_OPERATION_FOR_DOMAIN % (
										operation_name, domain, e
									)
									logger.error(errmsg)
									err_zones[domain] = errmsg
					else:
						logger.error(
							messages.UNABLE_TO_PERFORM_DNS_FORWARDING_OPERATION_FOR_ALL_CDNS_NOT_DESCRIBED,
							operation_name, plesk_id, dns_server_ip
						)
						for zone in domains_masters:
							errmsg = (
								messages.UNABLE_TO_PERFORM_DNS_FORWARDING_OPERATION_FOR_DOMAIN_CDNS_NOT_DESCRIBED % (
									operation_name, zone, dns_server_ip
								)
							)
							err_zones[zone] = errmsg
		except Exception as e:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			logger.error(messages.FAILED_TO_PERFORM_OPERATION_FOR_ALL_DOMAINS, operation_name, plesk_id, e)
			for zone in domains_masters:
				errmsg = (
					messages.UNABLE_TO_PERFORM_DNS_FORWARDING_OPERATION_FOR_DOMAIN_CNDS_FAILURE % (operation_name, zone, e)
				)
				err_zones[zone] = errmsg

		return err_zones

	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, messages.UNDO_DNS_FORWARDING))

		return err_zones

	@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)


class MigrationListSourceData(object):
	def __init__(self, plesks, expand_objects):
		self._plesks = plesks
		self._expand_objects = expand_objects

	@property
	def plesks(self):
		return self._plesks

	@property
	def expand_objects(self):
		"""
		:rtype: parallels.plesk.source.expand.expand_data.model.Model
		"""
		return self._expand_objects
