import logging
from collections import namedtuple

from parallels.ppa import messages
from parallels.core.converter.business_objects.common import SOURCE_TARGET
from parallels.core.import_api.model import WebspaceStatus
from parallels.ppa.utils.xml_rpc.poa import poa_api
from parallels.core.utils.common import partition, try_until, if_not_none, group_by_id, group_by, obj, poll_data,\
	polling_intervals
from parallels.core.utils.steps_profiler import sleep
import parallels.plesk.utils.xml_rpc.plesk.operator
import parallels.plesk.utils.xml_rpc.plesk.operator.role
import parallels.plesk.utils.xml_rpc.plesk.operator.reseller
import parallels.plesk.utils.xml_rpc.plesk.operator.site
from parallels.core.utils import unix_utils
from parallels.ppa.utils import poa_api_helper
from parallels.ppa.utils import poa_utils
from parallels.core.utils import migrator_utils


plesk_api = parallels.plesk.utils.xml_rpc.plesk

PollingMaxTimes = namedtuple('PollingMaxTimes', ('client', 'subscription'))

logger = logging.getLogger(__name__)


class PPAClientAndSubscriptionImporter(object):
	"""
	Class responsible for import of target data model to target system (Plesk or PPA):
	everything above hosting settings is imported there
	"""
	def __init__(self, import_api, conn, safe, polling_max_times=None):
		"""
		ppa - instance of common.ppa_api.ClientWrapper
		conn - instance of common.connections.ConnectionsTargetPPA
		"""
		self.import_api = import_api  # one of implementations of PPA API wrapper
		self.conn = conn
		self.safe = safe
		if polling_max_times is None:
			self.polling_max_times = PollingMaxTimes(
				client=2 * 60,  # 2 minutes per one client
				subscription=10 * 60,  # 10 minutes per one subscription
			)


	def import_clients_and_subscriptions(
		self, model, existing_objects, clean_up_vhost_skeleton, subscription_source_mail_ips
	):
		subscriptions_to_create = []
		ppa_customers = {}  # { id: login }
		webspaces_by_name = group_by_id(existing_objects.webspaces, lambda ws: ws.name)
		for reseller_login, client in self._list_all_clients(model):
			vendor_id = existing_objects.resellers[reseller_login].id if reseller_login is not None else poa_api.Identifiers.OID_ADMIN
			if (
				# customer disappeared since fetch_target(),
				# or since PBAS created it
				(
					client.source == SOURCE_TARGET
					and 
					client.login not in existing_objects.customers
				)
				and 
				# customer is not administrator/reseller
				client.login != reseller_login
			):	
				self.safe.fail_client(reseller_login, client.login, messages.CUSTOMER_ACCOUNT_LOGIN_S_MUST_EXIST % client.login)
				continue
			with self.safe.try_client(reseller_login, client.login, messages.FAILED_CREATE_CLIENT_IN_S % self.import_api.system_name):
				if client.login not in existing_objects.customers:
					if client.login != reseller_login:
						logger.info(messages.LOG_CREATE_CLIENT, client.login)
						client_id = self.import_api.create_customer(vendor_id, client)
					else:
						client_id = vendor_id
				else:
					logger.info(messages.CLIENT_S_ALREADY_EXISTS_IN_PPA, client.login)
					client_id = existing_objects.customers[client.login].id
				ppa_customers[client_id] = client.login

				for subscription in client.subscriptions:
					# XXX this code is added for repeatability, but clearly the check is missing, if it's the same webspace
					sub_id = subscription.sub_id
					if sub_id is None and subscription.name in webspaces_by_name:
						sub_id = webspaces_by_name[subscription.name].webspace_id

					if sub_id is None:
						if subscription.plan_id is not None:
							plan_id = subscription.plan_id 
						else:
							if subscription.plan_name is not None:
								plan_id = poa_api_helper.get_service_template_by_owner_and_name(
									existing_objects.service_templates, vendor_id, subscription.plan_name
								).st_id	 # XXX this question with plan_id must be clarified in converter.
							else:
								plan_id = None  # custom subscription - subscription that is not assigned to any service template (service plan)
						subscriptions_to_create.append(obj(
							client_id=client_id, plan_id=plan_id, subscription=subscription,
							client=client
						))
					else:
						logger.debug(messages.SUBSCRIPTION_S_ALREADY_EXISTS_IN_PPA, subscription.name)
						if if_not_none(webspaces_by_name.get(subscription.name), lambda ws: ws.owner_id) != if_not_none(existing_objects.customers.get(client.login, None), lambda c: c.id):
							logger.debug(messages.PPA_WEBSPACE_CORRESPONDING_SUBSCRIPTION_S_DIFFERENT % subscription.name)

		client_login_to_guid = self._wait_for_clients_provisioning([
			(r, c.login) for r, c in self._list_all_clients(model)
			if c.login != r
		])

		# Create all necessary groups, if they do not exist yet.
		# subscription.group_name must be set: either to subscription.name or to plan_name (Plesk)
		# or, additionally, to group_name (PBAS)
		groups = {}  # { (client_id, plan_id, group_name) : group_id }
		for s in subscriptions_to_create:
			if (s.client_id, s.plan_id, s.subscription.group_name) in groups:
				continue  # group is already created and we have its identifier for further use

			matching_groups = [
				subscr for subscr in existing_objects.raw_target_subscriptions if all([
					subscr.owner_id == s.client_id,
					subscr.st_id == s.plan_id,
					subscr.name == s.subscription.group_name,
				])
			]
			if len(matching_groups) > 0:
				groups[(s.client_id, s.plan_id, s.subscription.group_name)] = matching_groups[0].subscription_id
				continue	# group is already created and we have just obtained its identifier for further use

			if s.subscription.plan_id is not None or s.subscription.plan_name is not None:
				plan_description = u"#%s" % s.subscription.plan_id if s.subscription.plan_id is not None else u"'%s'" % s.subscription.plan_name
				plan_description = ' to service template %s' % (plan_description,)
			else:
				plan_description = ''
			logger.info(
				messages.LOG_CREATE_SUBSCRIPTION, s.subscription.group_name,
				plan_description, ppa_customers[s.client_id]
			)
			with self.safe.try_subscription(
				s.subscription.name, messages.FAILED_CREATE_SUBSCRIPTION_IN_S % self.import_api.system_name
			):
				groups[(s.client_id, s.plan_id, s.subscription.group_name)] = self.import_api.create_hosting_subscription(
					s.client_id, s.plan_id, s.subscription.addon_plan_ids, s.subscription, s.client
				)

		# Create all necessary webspaces
		subscriptions_by_group = group_by(subscriptions_to_create, lambda s: groups.get((s.client_id, s.plan_id, s.subscription.group_name)))
		freshly_provisioned_webspaces = []  # list of webspaces that should be cleaned up
		for group_id in subscriptions_by_group:
			if group_id is None:
				continue  # skip subscriptions for which PPA subscription creation has failed
			for s in subscriptions_by_group[group_id]:
				with self.safe.try_subscription(s.subscription.name, messages.FAILED_CREATE_WEBSPACE_IN_S % self.import_api.system_name):
					logger.info(messages.CREATE_WEBSPACE_S_IN_SUBSCRIPTION_S, s.subscription.name, group_id)
					s.subscription.group_id = group_id

					webspace_id = self.import_api.create_webspace(s.subscription)
					status = self._wait_for_webspace_provisioning(s.subscription.name, webspace_id)
					if status in [WebspaceStatus.ACTIVE, WebspaceStatus.SUSPENDED]:
						freshly_provisioned_webspaces.append(s.subscription.name)

		# Clean up the freshly created webspaces
		if clean_up_vhost_skeleton:
			for _, client in self._list_all_clients(model):
				for subscription in client.subscriptions:
					# filter: do cleanup only for subscriptions that were successfully created in this turn
					if subscription.name in freshly_provisioned_webspaces:
						with self.safe.try_subscription(subscription.name, messages.FAILED_CLEAN_UP_SKELETON_WEBSPACE):
							self._cleanup_subscription_vhost_files(subscription.name, subscription.is_windows)
		
		subscription_names = [s.name for _, client in self._list_all_clients(model) for s in client.subscriptions]
		self._create_auxiliary_roles(self._list_all_clients(model), client_login_to_guid, existing_objects)
		self._create_auxiliary_users(self._list_all_clients(model), client_login_to_guid, existing_objects, subscription_source_mail_ips, subscription_names)

	def _cleanup_subscription_vhost_files(self, subscription_name, is_windows):
		logger.debug(messages.CLEAN_UP_SUBSCRIPTIONS_VHOST_FILES)
		if not self._is_virtual_hosting_subscription(self.conn.plesk_api(), subscription_name):
			logger.debug(messages.SUBSCRIPTION_DOES_NOT_HAVE_VIRTUAL_HOSTING)
			return

		ips = self._get_subscription_web_ips(self.conn.plesk_api(), subscription_name)
		if ips is None:
			logger.debug(messages.SUBSCRIPTION_NO_WEB_IP_ADDRESSES_SKIP)
			return

		logger.debug(messages.DEBUG_SUBSCRIPTION_WEB_IPS, ips)
		ip = ips.v4 or ips.v6

		ppa_host_id = poa_utils.get_host_id_by_ip(self.conn.poa_api(), ip)
		logger.debug(messages.PPA_HOST_ID_WHERE_SUBSCRIPTIONS_WEB, ppa_host_id)

		if is_windows:
			with self.conn.ppa_windows_node_runner(ppa_host_id) as runner:
				migrator_utils.clean_up_vhost_dir(
					subscription_name, runner, is_windows
				)
		else:
			with self.conn.ppa_unix_node_runner(ppa_host_id) as runner:
				migrator_utils.clean_up_vhost_dir(
					subscription_name, runner, is_windows
				)

	@staticmethod
	def _get_subscription_web_ips(plesk_api, subscription_name):
		subscription_infos = plesk_api.send(
			parallels.plesk.utils.xml_rpc.plesk.operator.SubscriptionOperator.Get(
				parallels.plesk.utils.xml_rpc.plesk.operator.SubscriptionOperator.FilterByName([subscription_name]),
				[
					parallels.plesk.utils.xml_rpc.plesk.operator.SubscriptionOperator.Dataset.HOSTING_BASIC,
				]
			)
		)

		if len(subscription_infos) != 1:
			raise Exception(
				messages.ERROR_WHILE_FETCHING_SUBSCRIPTION_WEB_SERVICE % 
				len(subscription_infos)
			)

		subscription_info = subscription_infos[0].data[1]

		return subscription_info.hosting.ip_addresses if hasattr(subscription_info.hosting, 'ip_addresses') else None

	@staticmethod
	def _is_virtual_hosting_subscription(plesk_api, subscription_name):
		results = plesk_api.send(
			parallels.plesk.utils.xml_rpc.plesk.operator.SiteOperator.Get(
				filter=parallels.plesk.utils.xml_rpc.plesk.operator.SiteOperator.FilterByName([subscription_name]),
				dataset=[parallels.plesk.utils.xml_rpc.plesk.operator.SiteOperator.Dataset.GEN_INFO]
			)
		)

		if len(results) != 1:
			raise Exception(messages.FAILED_GET_HOSTING_TYPE_SUBSCRIPTION_S % (subscription_name, len(results)))

		return results[0].data[1].gen_info.htype == 'vrt_hst'

	@staticmethod
	def _get_subscription_name_to_id_for_client(plesk_api, client_login):
		return dict((result.data[1].gen_info.name, result.data[0])
			for result in plesk_api.send(
				parallels.plesk.utils.xml_rpc.plesk.operator.SubscriptionOperator.Get(
					parallels.plesk.utils.xml_rpc.plesk.operator.SubscriptionOperator.FilterByOwnerLogin([client_login]),
					[
						parallels.plesk.utils.xml_rpc.plesk.operator.SubscriptionOperator.Dataset.GEN_INFO,
					]
				)
			)
		)
		
	def _wait_for_clients_provisioning(self, clients):
		logger.info(messages.WAIT_UNTIL_ALL_CLIENTS_ARE_CREATED)

		reseller_by_client = dict((client_login, reseller) for reseller, client_login in clients)
		clients_remained = set([client_login for _, client_login in clients])

		client_guids = {}
		def check_clients():
			from parallels.plesk.utils.xml_rpc.plesk.operator.customer import CustomerOperator
			results = self.conn.plesk_api().send(CustomerOperator.Get(
				filter = CustomerOperator.FilterByLogin(clients_remained),
				dataset = frozenset([CustomerOperator.Dataset.GEN_INFO])
			))
			
			new_clients = dict((r.data[1].login, r.data[1].guid) for r in results if r.ok)
			if new_clients:
				logger.debug(messages.DEBUG_CLIENTS_PROVISIONED, len(new_clients), new_clients)
				client_guids.update(new_clients)
				clients_remained.difference_update(new_clients.keys())

			if any(r.code != CustomerOperator.ERR_CLIENT_DOES_NOT_EXIST for r in results if not r.ok):
				raise Exception(messages.SOME_ERROR_OCCURED_DURING_FETCHING_LIST)

			return len(clients_remained) == 0

		try_until(
			check_clients, 
			polling_intervals(
				starting=5, 
				max_attempt_interval=60, 
				max_total_time=self.polling_max_times.client*len(clients)
			),
			sleep=lambda t: sleep(t, messages.WAITING_FOR_CLIENT_PROVISIONING)
		)
		for client in clients_remained:
			self.safe.fail_client(reseller_by_client[client], client, messages.TIME_OUT_WAITING_FOR_PROVISIONING_CUSTOMER)
		if len(clients_remained) == 0:
			logger.info(messages.ALL_CLIENTS_ARE_PROVISIONED_PLESK)
		else:
			logger.error(messages.TIME_OUT_WAITING_FOR_PROVISIONING_CUSTOMERS, len(clients_remained), len(clients))

		return client_guids

	def _wait_for_webspace_provisioning(self, webspace_name, webspace_id):
		def get_webspace_status():
			status, status_message = self.import_api.get_request_status_for_webspace(webspace_id)

			if status == WebspaceStatus.FAILED:
				self.safe.fail_subscription(
					webspace_name, messages.WEBSPACE_PROVISIONING_FAILED % (webspace_name, status_message)
				)

			return status if status != WebspaceStatus.PENDING else None

		status = poll_data(
			get_webspace_status, 
			polling_intervals(
				starting=5, 
				max_attempt_interval=60,
				max_total_time=self.polling_max_times.subscription
			),
			sleep=lambda t: sleep(t, messages.WAITING_FOR_WEBSPACE_PROVISIONING)
		)

		if status is None:  # WebspaceStatus.PENDING
			logger.warning(messages.PROVISIONING_WEBSPACE_S_IS_TIMED_OUT % webspace_name)
			self.safe.fail_subscription(
				webspace_name,
				messages.TIMED_OUT_WAITING_UNTIL_WEBSPACE_S % webspace_name,
				solution=messages.HELP_PPA_COMPLETE_TASKS_FOR_WEBSPACE
			)
		return status

	def _create_auxiliary_users(self, all_clients, client_login_to_guid, existing_objects, subscription_source_mail_ips, subscription_names):
		active_poa_webspaces = set([
			webspace.name for webspace in self.import_api.list_webspaces(subscription_names)
			if webspace.status == WebspaceStatus.ACTIVE or webspace.status is None
		])

		with self.conn.main_node_runner() as main_node_runner:
			for reseller_name, client in all_clients:
				if client.login == reseller_name:
					return

				with self.safe.try_client(reseller_name, client.login, messages.FAILED_CREATE_AUXILIARY_USERS_CLIENT):
					if len(client.auxiliary_users) > 0:
						logger.info(messages.CREATE_AUXILIARY_USERS_FOR_S_CUSTOMER % client.login)

						client_subscriptions = self._get_subscription_name_to_id_for_client(self.conn.plesk_api(), client.login)
						client_active_subscriptions = active_poa_webspaces & set(client_subscriptions.keys())

						for user in client.auxiliary_users:
							with self.safe.try_auxiliary_user(client.login, user.login, messages.FAILED_CREATE_AUXILIARY_USER_IN_PLESK):
								if (client.login, user.login) not in existing_objects.auxiliary_users:
									if client_subscriptions:
										if user.subscription_name is not None and user.subscription_name not in client_subscriptions:
											raise Exception(messages.SUBSCRIPTION_S_USER_S_IS_RESTRICTED % (user.subscription_name, user.login))
										elif user.subscription_name is not None and user.subscription_name not in client_active_subscriptions:
											raise Exception(messages.SUBSCRIPTION_S_USER_S_IS_RESTRICTED_1 % (user.subscription_name, user.login))
										else: 
											product_root_d = self.conn.plesk_server.plesk_dir

											disable_provisioning_env = '' # necessary for SmarterMail assimiation scenario
											if user.subscription_name is not None and subscription_source_mail_ips.get(user.subscription_name) == self._get_subscription_mail_ip(user.subscription_name):
												disable_provisioning_env = 'PLESK_DISABLE_APSMAIL_PROVISIONING=true '

											cmd = disable_provisioning_env + unix_utils.format_command(
												u"ALLOW_WEAK_PASSWORDS=1 {product_root_d}/bin/user --create {user_login} -domain-admin true "
												u"-cname {user_name} -role {role} -passwd {user_password} "
												u"-company {company} -phone {phone} -fax {fax} "
												u"-address {address} -city {city} -state {state} "
												u"-zip {zip} -country {country} -im {im} -im-type {imtype} -comment {comment} "
												u"-owner {client_login} -status {status} -email {email}",
												disable_provisioning_env=disable_provisioning_env,
												product_root_d=product_root_d,
												user_login=user.login,
												user_name=user.name,
												role=list(user.roles)[0], # XXX don't know how many roles for regular aux user
												user_password=user.password,
												company=user.personal_info.get('company', ''),
												phone=user.personal_info.get('phone', ''),
												fax=user.personal_info.get('fax', ''),
												address=user.personal_info.get('address', ''),
												city=user.personal_info.get('city', ''),
												state=user.personal_info.get('state', ''),
												zip=user.personal_info.get('zip', ''),
												country=user.personal_info.get('country', ''),
												im=user.personal_info.get('im', ''),
												imtype=user.personal_info.get('im-type', ''),
												comment=user.personal_info.get('comment', ''),
												client_login=client.login,
												status='enabled' if user.is_active else 'disabled',
												email=user.personal_info['email'],
											)
											if user.subscription_name is not None:
												cmd += u" -subscription-name '%s'" % user.subscription_name # XXX don't know if creating user without subscription works

											main_node_runner.sh(cmd)
									else:
										self.safe.fail_auxiliary_user(
											user.login, 
											client.login, 
											messages.CANNOT_CREATE_AUXILIARY_USERS_FOR_CLIENT)
								else:
									logger.info(messages.SKIP_AUXILIARY_USER_S_AS_IT, user.login)

	def _get_subscription_mail_ip(self, subscription_name):
		mail = self.conn.plesk_api().send(
			plesk_api.operator.SubscriptionOperator.Get(
				filter=plesk_api.operator.SubscriptionOperator.FilterByName([subscription_name]),
				dataset=[
					plesk_api.operator.SubscriptionOperator.Dataset.GEN_INFO,
					plesk_api.operator.SubscriptionOperator.Dataset.HOSTING_BASIC,
					plesk_api.operator.SubscriptionOperator.Dataset.MAIL,
				]
			)
		)[0].data[1].mail

		if mail is not None:
			return mail.ip_addresses.v4

	def _create_auxiliary_roles(self, all_clients, client_login_to_guid, existing_objects):
		for reseller_name, client in all_clients:
			if client.login == reseller_name:
				return

			with self.safe.try_client(reseller_name, client.login, messages.FAILED_CREATE_AUXILIARY_ROLES_CLIENT):
				client_guid = client_login_to_guid[client.login]

				roles_names = set(role.name for role in client.auxiliary_user_roles)
				roles_by_names = dict((role.name, role) for role in client.auxiliary_user_roles)
				
				default_permissions = self._get_role_default_permissions(client_guid)

				if client.login not in existing_objects.customers:
					existing_built_in_roles_names, existing_non_built_in_roles_names = [
						set([item.data.name for item in group]) for group in partition(
							self._get_client_roles(client_guid),
							lambda x: x.data.is_built_in == True 
						)
					]

					logger.debug(messages.DEBUG_CREATING_ROLES)
					for role_name in roles_names - existing_built_in_roles_names - existing_non_built_in_roles_names:
						with self.safe.try_auxiliary_user_role(
							client.login, role_name, messages.FAILED_CREATE_AUXILIARY_USER_ROLE_PLESK
						):
							role = roles_by_names[role_name]
							self._create_auxiliary_role_with_plesk_api(client_guid, default_permissions, role)

					logger.debug(messages.UPDATING_ROLES_PRECREATED_BY_PLESK_BUT)
					for role_name in roles_names & existing_non_built_in_roles_names:
						with self.safe.try_auxiliary_user_role(
							client.login, role_name, messages.FAILED_UPDATE_AUXILIARY_USER_ROLE_PLESK
						):
							role = roles_by_names[role_name]
							self._update_auxiliary_role_with_plesk_api(client_guid, default_permissions, role)
				else:
					for role_name in roles_names:
							if (client.login, role_name) not in existing_objects.auxiliary_user_roles:
								with self.safe.try_auxiliary_user_role(
									client.login, role_name, messages.FAILED_CREATE_AUXILIARY_USER_ROLE_PLESK_1
								):
									role = roles_by_names[role_name]
									self._create_auxiliary_role_with_plesk_api(client_guid, default_permissions, role)
							else:
								logger.info(messages.SKIP_AUXILIARY_USER_ROLE_S_AS, role_name)

	def _create_auxiliary_role_with_plesk_api(self, client_guid, default_permissions, role):
		permissions = dict(default_permissions.items() + role.permissions.items())
		return self.conn.plesk_api().send(
			plesk_api.operator.role.RoleOperator.Add(
				name=role.name,
				owner_guid=client_guid,
				permissions=permissions
			)
		).data

	def _update_auxiliary_role_with_plesk_api(self, client_guid, default_permissions, role):
		permissions = dict(default_permissions.items() + role.permissions.items())
		return self.conn.plesk_api().send(
			plesk_api.operator.role.RoleOperator.Set(
				plesk_api.operator.role.RoleOperator.FilterName([role.name]),
				owner_guid=client_guid,
				permissions=permissions
			)
		).data

	def _get_role_default_permissions(self, client_guid):
		# default permissions are necessary at least when you have an empty permissions set in Plesk backup
		# (Plesk backup contains only permissions that differ from defaults, so this situation is possible),
		# but Plesk API does not allow permissions set to be empty
		return dict(
			(perm.name, perm.default_value)
			for perm in self.conn.plesk_api().send(
				plesk_api.operator.role.RoleOperator.GetPermissionDescriptor(
					plesk_api.operator.role.RoleOperator.GetPermissionDescriptor.FilterOwnerGuid([client_guid]),
				)
			).data 
			if perm.type == 'boolean'
		)

	def _get_client_roles(self, client_guid):				
		return self.conn.plesk_api().send(
			plesk_api.operator.role.RoleOperator.Get(
				filter=plesk_api.operator.role.RoleOperator.FilterAll(),
				owner_guid=client_guid
			)
		)

	def _list_all_clients(self, model):
		reseller_clients = []
		for reseller in model.resellers.itervalues():
			for client in reseller.clients:
				reseller_clients.append((reseller.login, client))
		admin_clients = [(None, client) for client in model.clients.itervalues()]

		return reseller_clients + admin_clients
