from xml.etree import ElementTree

from parallels.core.registry import Registry
from parallels.core.utils.plesk_cli_runner import PleskCLIRunnerCLI
from parallels.plesk.utils.xml_rpc.plesk.operator import ServicePlanLogRotation, ServicePlanLogRotationSettings, \
	ServicePlanLogRotationConditionBySize, ServicePlanLogRotationConditionByTime, ServicePlanMail, \
	ServicePlanMailNonexistentUserForward, ServicePlanMailNonexistentUserBounce, ServicePlanMailNonexistentUserReject, \
	ServicePlanPreferences, ServicePlanHostingSetting, ServicePlanPerformance, ServicePlanWebServerSettings, \
	ServicePlanApsPackage, ServicePlanAddonPerformance, ServicePlanAddonHostingSetting, ServicePlanAddonApsPackage, \
	ServicePlanAddonLimits
from parallels.plesk.models.target_data_model import HostingPlanLogRotationConditionByTime, \
	HostingPlanLogRotationConditionBySize, HostingPlanMailNonexistentUserBounce, HostingPlanMailNonexistentUserForward, \
	HostingPlanMailNonexistentUserReject

from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.core.utils.common import if_not_none


class HostingPlansImporter(object):
	"""Import hosting plans (addons and regular ones) to target Plesk panel with Plesk API/CLI"""

	def __init__(self, conn):
		"""
		:type conn: parallels.plesk.connections.target_connections.PleskTargetConnections
		"""
		self._conn = conn

	def create_plan(self, owner_login, plan_name, plan_settings):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		log_rotation = self._create_log_rotation_api_info(plan_settings)
		plesk_api = self._conn.plesk_api()

		plesk_api.send(
			plesk_ops.ServicePlanOperator.Add(
				name=plan_name,
				owner_login=owner_login,
				limits=plesk_ops.ServicePlanLimits(
					overuse=plan_settings.overuse,
					limits=[
						plesk_ops.ServicePlanLimit(name, value)
						for name, value in plan_settings.limits.iteritems()
					]
				),
				permissions=[
					plesk_ops.ServicePlanPermission(name, value)
					for name, value in plan_settings.permissions.iteritems()
				],
				php_settings=None,
				log_rotation=log_rotation,
				mail=self._create_mail_api_info(plan_settings),
				preferences=self._create_preferences_api_info(plan_settings),
				hosting=self._create_hosting_api_info(plan_settings),
				performance=self._create_performance_api_info(plan_settings),
				web_server_settings=self._create_web_server_settings_api_info(plan_settings),
			)
		).check()

		self._create_aps_bundle_filter(owner_login, plan_name, plan_settings, plesk_api)
		self._create_custom_plan_items(owner_login, plan_name, plan_settings)
		self._configure_default_db_servers(owner_login, plan_name, plan_settings)
		self._set_php_settings(owner_login, plan_name, plan_settings.php_settings_node, is_addon=False)
		self._set_no_hosting(owner_login, plan_name, plan_settings)

	def create_addon_plan(self, owner_login, plan_name, plan_settings):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		self._conn.plesk_api().send(
			plesk_ops.ServicePlanAddonOperator.Add(
				name=plan_name,
				owner_login=owner_login,
				limits=ServicePlanAddonLimits(
					overuse=plan_settings.overuse,
					limits=[
						plesk_ops.ServicePlanAddonLimit(name, value)
						for name, value in plan_settings.limits.iteritems()
					]
				),
				permissions=[
					plesk_ops.ServicePlanAddonPermission(name, value)
					for name, value in plan_settings.permissions.iteritems()
				],
				hosting=self._create_addon_hosting_api_info(plan_settings),
				performance=self._create_addon_performance_api_info(plan_settings),
				php_settings=None,
			)
		).check()
		self._create_addon_aps_bundle_filter(owner_login, plan_name, plan_settings)
		self._set_php_settings(owner_login, plan_name, plan_settings.php_settings_node, is_addon=True)

	@staticmethod
	def _create_log_rotation_api_info(plan_settings):
		"""Create API request part from import API object for service plan log rotation

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanLogRotation
		"""
		if plan_settings.log_rotation is not None:
			if plan_settings.log_rotation.settings is not None:
				source_log_condition = plan_settings.log_rotation.settings.log_condition
				if isinstance(source_log_condition, HostingPlanLogRotationConditionByTime):
					log_condition = ServicePlanLogRotationConditionByTime(
						source_log_condition.time
					)
				elif isinstance(source_log_condition, HostingPlanLogRotationConditionBySize):
					log_condition = ServicePlanLogRotationConditionBySize(
						source_log_condition.size
					)
				else:
					assert False
			else:
				log_condition = None

			log_rotation = ServicePlanLogRotation(
				status=plan_settings.log_rotation.status,
				settings=if_not_none(
					plan_settings.log_rotation.settings,
					lambda s: ServicePlanLogRotationSettings(
						log_condition=log_condition,
						log_max_num_files=s.log_max_num_files,
						log_compress=s.log_compress,
						log_email=s.log_email
					)
				)
			)
		else:
			log_rotation = None

		return log_rotation

	@staticmethod
	def _create_mail_api_info(plan_settings):
		"""Create API request part from import API object for service plan mail settings

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanLogRotation
		"""
		if plan_settings.mail is None:
			return None

		if plan_settings.mail.nonexistent_user is not None:
			if isinstance(plan_settings.mail.nonexistent_user, HostingPlanMailNonexistentUserBounce):
				nonexistent_user = ServicePlanMailNonexistentUserBounce(
					plan_settings.mail.nonexistent_user.message
				)
			elif isinstance(plan_settings.mail.nonexistent_user, HostingPlanMailNonexistentUserForward):
				nonexistent_user = ServicePlanMailNonexistentUserForward(
					plan_settings.mail.nonexistent_user.address
				)
			elif isinstance(plan_settings.mail.nonexistent_user, HostingPlanMailNonexistentUserReject):
				nonexistent_user = ServicePlanMailNonexistentUserReject()
			else:
				assert False
		else:
			nonexistent_user = None

		return ServicePlanMail(
			nonexistent_user=nonexistent_user,
			webmail=plan_settings.mail.webmail,
		)

	@staticmethod
	def _create_preferences_api_info(plan_settings):
		"""Create API request part from import API object for service plan preferences

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanPreferences
		"""
		if plan_settings.preferences is None:
			return None

		return ServicePlanPreferences(
			stat=plan_settings.preferences.stat,
			maillists=plan_settings.preferences.maillists,
			mailservice=plan_settings.preferences.mailservice,
			dns_zone_type=plan_settings.preferences.dns_zone_type,
		)

	@staticmethod
	def _create_hosting_api_info(plan_settings):
		"""Create API request part from import API object for service plan hosting settings

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: list[parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanHostingSetting] | None
		"""
		if plan_settings.hosting is None:
			return None

		return [
			ServicePlanHostingSetting(hosting.name, hosting.value)
			for hosting in plan_settings.hosting
		]

	@staticmethod
	def _create_addon_hosting_api_info(plan_settings):
		"""Create API request part from import API object for service plan hosting settings

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: list[parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanAddonHostingSetting] | None
		"""
		if plan_settings.hosting is None:
			return None

		return [
			ServicePlanAddonHostingSetting(hosting.name, hosting.value)
			for hosting in plan_settings.hosting
		]

	@staticmethod
	def _create_performance_api_info(plan_settings):
		"""Create API request part from import API object for service plan performance settings

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanPerformance
		"""
		if plan_settings.performance is None:
			return None
		return ServicePlanPerformance(
			bandwidth=plan_settings.performance.bandwidth,
			max_connections=plan_settings.performance.max_connections,
		)

	@staticmethod
	def _create_addon_performance_api_info(plan_settings):
		"""Create API request part from import API object for service plan performance settings

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanAddonPerformance
		"""
		if plan_settings.performance is None:
			return None
		return ServicePlanAddonPerformance(
			bandwidth=plan_settings.performance.bandwidth,
			max_connections=plan_settings.performance.max_connections,
		)

	@staticmethod
	def _create_web_server_settings_api_info(plan_settings):
		"""Create API request part from import API object for service plan web server settings

		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanWebServerSettings
		"""
		if plan_settings.web_server_settings is None:
			return None
		return ServicePlanWebServerSettings(
			additional=plan_settings.web_server_settings.additional,
			additional_ssl=plan_settings.web_server_settings.additional_ssl,
			additional_nginx=plan_settings.web_server_settings.additional_nginx
		)

	@staticmethod
	def _create_aps_bundle_filter(owner_login, plan_name, plan_settings, plesk_api):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		if plan_settings.aps_bundle_filter is None:
			return

		if plan_settings.aps_bundle_filter.type != 'white':
			# Other types of APS filters are not supported
			return

		api_filter = plesk_ops.ServicePlanOperator.FilterByName([plan_name])
		enable_aps_filter_request = plesk_ops.ServicePlanOperator.EnableApsFilter(
			filter=api_filter, owner_login=owner_login
		)
		plesk_api.send(enable_aps_filter_request).check()
		add_package_request = plesk_ops.ServicePlanOperator.AddPackage(
			filter=api_filter,
			owner_login=owner_login,
			packages=[
				ServicePlanApsPackage(name=package.name, value=package.value)
				for package in plan_settings.aps_bundle_filter.items
			]
		)
		plesk_api.send(add_package_request).check()

	def _create_addon_aps_bundle_filter(self, owner_login, plan_name, plan_settings):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		if plan_settings.aps_bundle_filter is None:
			return

		if plan_settings.aps_bundle_filter.type != 'white':
			# Other types of APS filters are not supported
			return

		plesk_api = self._conn.plesk_api()
		api_filter = plesk_ops.ServicePlanOperator.FilterByName([plan_name])
		request = plesk_ops.ServicePlanAddonOperator.AddPackage(
			filter=api_filter,
			owner_login=owner_login,
			packages=[
				ServicePlanAddonApsPackage(name=package.name, value=package.value)
				for package in plan_settings.aps_bundle_filter.items
			]
		)
		plesk_api.send(request).check()

	def _set_php_settings(self, owner_login, plan_name, php_settings_node, is_addon):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:rtype: None
		"""
		if php_settings_node is None:
			return

		migration_server = Registry.get_instance().get_context().migrator_server
		remote_plan_settings_file = self._conn.plesk_server.get_session_file_path('plan_php_settings.xml')
		local_plan_settings_file = migration_server.get_session_file_path('plan_php_settings.xml')
		file_contents = ElementTree.tostring(php_settings_node, 'utf-8', 'xml')
		args = [
			'--set-php-settings', plan_name,
			'-settings', remote_plan_settings_file
		]
		if owner_login is not None:
			args.extend(['-owner', owner_login])

		with migration_server.runner() as runner:
			runner.upload_file_content(
				local_plan_settings_file, file_contents
			)

		with self._conn.plesk_server.runner() as runner:
			runner.upload_file(local_plan_settings_file, remote_plan_settings_file)
			command = 'domain_template' if not is_addon else 'domain_addon_service_plan'
			plesk_cli_runner = PleskCLIRunnerCLI(self._conn.plesk_server)
			plesk_cli_runner.run(command, args)
			runner.remove_file(remote_plan_settings_file)

		with migration_server.runner() as runner:
			runner.remove_file(local_plan_settings_file)

	def _create_custom_plan_items(self, owner_login, plan_name, plan_settings):
		"""Create custom plan items (also known as "Additional Services" in Plesk) for specified plan

		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		for custom_plan_item in plan_settings.custom_plan_items:
			if not self._custom_plan_item_exists(custom_plan_item):
				self._create_custom_plan_item(custom_plan_item)
			self._assign_template_to_custom_plan_item(owner_login, plan_name, custom_plan_item)

	def _custom_plan_item_exists(self, custom_plan_item):
		"""Check if custom plan item exists on target

		:type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
		:rtype: bool
		"""
		existing_plan_items_list = self._conn.plesk_cli_runner.run(
			'custom_plan_items', ['--list']
		)
		# Simply check presence of GUID in list command output. There should be better way.
		return custom_plan_item.guid in existing_plan_items_list

	def _create_custom_plan_item(self, custom_plan_item):
		"""Create custom plan item in target Plesk

		:type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
		:rtype: None
		"""
		arguments = [
			'--add',
			custom_plan_item.name,
			'-uuid', custom_plan_item.guid,
			'-visible', custom_plan_item.visible,
		]
		if custom_plan_item.description is not None:
			arguments.extend([
				'-label', custom_plan_item.description,
			])
		if custom_plan_item.hint is not None:
			arguments.extend([
				'-tooltip', custom_plan_item.hint,
			])
		if custom_plan_item.url is not None:
			arguments.extend([
				'-url', custom_plan_item.url
			])
		if custom_plan_item.options is not None:
			arguments.extend([
				'-url-components', ','.join(self._get_custom_plan_item_url_components(custom_plan_item))
			])

		self._conn.plesk_cli_runner.run(
			'custom_plan_items', arguments
		)

	@staticmethod
	def _get_custom_plan_item_url_components(custom_plan_item):
		"""Get URL component names list from numeric value from backup

		:type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
		:rtype: Iterable[basestring]
		"""
		# constants from pmm_deploy.h
		url_component_constants = dict(
			CUSTOM_BUTTON_DOM_ID=1,
			CUSTOM_BUTTON_CL_ID=2,
			CUSTOM_BUTTON_CNAME=4,
			CUSTOM_BUTTON_PNAME=8,
			CUSTOM_BUTTON_EMAIL=16,
			CUSTOM_BUTTON_DOM_NAME=32,
			CUSTOM_BUTTON_INTERNAL=256,
			CUSTOM_BUTTON_FTP_USER=512,
			CUSTOM_BUTTON_FTP_PASS=1024,
		)

		option_url_components = dict(
			CUSTOM_BUTTON_DOM_ID='dom_id',
			CUSTOM_BUTTON_DOM_NAME='dom_name',
			CUSTOM_BUTTON_FTP_USER='ftp_user',
			CUSTOM_BUTTON_FTP_PASS='ftp_pass',
			CUSTOM_BUTTON_CL_ID='cl_id',
			CUSTOM_BUTTON_CNAME='cname',
			CUSTOM_BUTTON_PNAME='pname',
			CUSTOM_BUTTON_EMAIL='email'
		)

		url_components = []
		for option_id, url_component_name in option_url_components.iteritems():
			if url_component_constants[option_id] & custom_plan_item.options != 0:
				url_components.append(url_component_name)

		return url_components

	def _assign_template_to_custom_plan_item(self, owner_login, plan_name, custom_plan_item):
		"""Assign service template to  custom plan item in target Plesk

		:type owner_login: basestring | None
		:type plan_name: basestring
		:type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
		:rtype: None
		"""
		args = [
			'--add-custom-plan-item', plan_name,
			'-custom-plan-item-name', custom_plan_item.name
		]
		if owner_login is not None:
			args.extend(['-owner', owner_login])
		self._conn.plesk_cli_runner.run('domain_template', args)

	def _configure_default_db_servers(self, owner_login, plan_name, plan_settings):
		"""
		:type owner_login: basestring | None
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		if plan_settings.default_db_servers is None:
			return

		available_plesk_servers = {
			(server.dbtype, server.host, str(server.port)): server.id
			for server in self._conn.plesk_server.get_db_servers().itervalues()
		}
		for default_db_server in plan_settings.default_db_servers:
			server_key = (default_db_server.type,  default_db_server.host, str(default_db_server.port))
			if server_key in available_plesk_servers:
				args = [
					'--update', plan_name,
					'-default_server_%s' % default_db_server.type,
					'%s:%s' % (default_db_server.host, default_db_server.port)
				]
				if owner_login is not None:
					args.extend(['-owner', owner_login])

				self._conn.plesk_cli_runner.run('domain_template', args)

	def _set_no_hosting(self, owner_login, plan_name, plan_settings):
		"""Configure plan that have no virtual hosting

		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		if plan_settings.virtual_hosting_enabled:
			return

		args = [
			'--update', plan_name,
			'-hosting', 'false'
		]
		if owner_login is not None:
			args.extend(['-owner', owner_login])
		self._conn.plesk_cli_runner.run('domain_template', args)