import logging
from collections import namedtuple

from parallels.common.import_api.model import WebspaceStatus
from parallels import poa_api
from parallels import utils
from parallels.utils import partition, try_until, if_not_none, group_by_id, group_by, obj, poll_data
from parallels.common.utils.steps_profiler import sleep
import parallels.plesk_api.operator
import parallels.plesk_api.operator.role
import parallels.plesk_api.operator.reseller
import parallels.plesk_api.operator.site
from parallels.common.utils import unix_utils, poa_api_helper
from parallels.common.utils import poa_utils
from parallels.common.utils import migrator_utils

plesk_api = parallels.plesk_api

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


class Importer(object):
	"""
	Class responsible for import of target data model to target system (Plesk or PPA):
	everything above hosting settings is imported there
	"""

	logger = logging.getLogger(__name__)

	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(
				reseller=2 * 60,  # 2 minutes per one reseller
				client=2 * 60,  # 2 minutes per one client
				subscription=10 * 60,  # 10 minutes per one subscription
			)

	def import_resellers(self, resellers):
		"""Import resellers stubs: only contact data, no subscription to reseller plan is created"""
		for reseller in resellers:
			with self.safe.try_reseller(reseller.login, "Failed to create reseller in %s." % self.import_api.system_name):
				if reseller.source != 'ppa':
					self.logger.info(u"Create reseller '%s'", reseller.login)
					self.import_api.create_reseller(reseller)

	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 == 'ppa' 
					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, u"Customer account with login '%s' must exist in PPA, but it does not exist." % client.login)
				continue
			with self.safe.try_client(reseller_login, client.login, u"Failed to create client in %s." % self.import_api.system_name):
				if client.login not in existing_objects.customers:
					if client.login != reseller_login:
						self.logger.info(u"Create client '%s'", client.login)
						client_id = self.import_api.create_customer(vendor_id, client)
					else:
						client_id = vendor_id
				else:
					self.logger.info(u"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:
						self.logger.debug(u"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):
							self.logger.debug(u"PPA webspace corresponding to subscription '%s' has different owner" % 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_ppa_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 = ''
			self.logger.info(u"Create subscription '%s'%s for customer '%s'", s.subscription.group_name, plan_description, ppa_customers[s.client_id])
			with self.safe.try_subscription(s.subscription.name, u"Failed to 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, u"Failed to create webspace in %s." % self.import_api.system_name):
					self.logger.info(u"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, u"Failed to clean up skeleton of 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):
		self.logger.debug(u"Clean up subscriptions vhost files")
		if not self._is_virtual_hosting_subscription(self.conn.plesk_api(), subscription_name):
			self.logger.debug(u"Subscription does not have virtual hosting, skip cleaning up its vhost")
			return

		ips = self._get_subscription_web_ips(self.conn.plesk_api(), subscription_name)
		if ips is None:
			self.logger.debug(u"Subscription has no web IP addresses, skip cleaning up its vhost")
			return

		self.logger.debug(u"Subscription web IPs: %r", ips)
		ip = ips.v4 or ips.v6

		ppa_host_id = poa_utils.get_host_id_by_ip(self.conn.poa_api(), ip)
		self.logger.debug(u"PPA host id where subscriptions web service is located: %s", 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_api.operator.SubscriptionOperator.Get(
				parallels.plesk_api.operator.SubscriptionOperator.FilterByName([subscription_name]),
				[
					parallels.plesk_api.operator.SubscriptionOperator.Dataset.HOSTING_BASIC,
				]
			)
		)

		if len(subscription_infos) != 1:
			raise Exception(
				u"Error while fetching subscription web service IP from Plesk: expected to receive exactly one subscription, Plesk API returned %d subscriptions." % 
				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_api.operator.SiteOperator.Get(
				filter=parallels.plesk_api.operator.SiteOperator.FilterByName([subscription_name]),
				dataset=[parallels.plesk_api.operator.SiteOperator.Dataset.GEN_INFO]
			)
		)

		if len(results) != 1:
			raise Exception(u"Failed to get hosting type of subscription %s: expected to have exactly 1 result to Plesk API request, got %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_api.operator.SubscriptionOperator.Get(
					parallels.plesk_api.operator.SubscriptionOperator.FilterByOwnerLogin([client_login]),
					[
						parallels.plesk_api.operator.SubscriptionOperator.Dataset.GEN_INFO,
					]
				)
			)
		)
		
	def _wait_for_clients_provisioning(self, clients):
		self.logger.info(u"Wait until all clients are created in Plesk")

		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_api.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:
				self.logger.debug(u"Following %d clients are provisioned: %r", 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(u"Some error occured during fetching list of provisioned clients. See log for Plesk response details.")

			return len(clients_remained) == 0

		try_until(
			check_clients, 
			utils.polling_intervals(
				starting=5, 
				max_attempt_interval=60, 
				max_total_time=self.polling_max_times.client*len(clients)
			),
			sleep=lambda t: sleep(t, 'Waiting for client provisioning')
		)
		for client in clients_remained:
			self.safe.fail_client(reseller_by_client[client], client, u"Time out: waiting for provisioning customer to Plesk")
		if len(clients_remained) == 0:
			self.logger.info(u"All clients are provisioned to Plesk")
		else:
			self.logger.error(u"Time out: waiting for provisioning of customers, %d out of %d customer(s) were not provisioned to Plesk", 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, u"Provisioning of the webspace %s is failed: %s" % (webspace_name, status_message))

			return status if status != WebspaceStatus.PENDING else None

		status = poll_data(
			get_webspace_status, 
			utils.polling_intervals(
				starting=5, 
				max_attempt_interval=60,
				max_total_time=self.polling_max_times.subscription
			),
			sleep=lambda t: sleep(t, 'Waiting for webspace provisioning')
		)

		if status is None:	# WebspaceStatus.PENDING
			self.logger.warning(u"Provisioning of the webspace %s is timed out." % webspace_name)
			self.safe.fail_subscription(webspace_name, u"Timed out waiting until webspace %s is provisioned" % webspace_name,
				solution=u"Help PPA to complete the tasks for this webspace successfully, then - only if you transfer subscription to a new server - clean up webspace's virtual host: remove all files from its httpdocs/ and cgi-bin/ directories, and then retry the migration of this subscription.")
		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, u"Failed to create auxiliary users of client."):
					if len(client.auxiliary_users) > 0:
						self.logger.info(u"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, u'Failed to 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(u"Subscription '%s' user '%s' is restricted to does not exist. User won't be created." % (user.subscription_name, user.login))
										elif user.subscription_name is not None and user.subscription_name not in client_active_subscriptions:
											raise Exception(u"Subscription '%s' user '%s' is restricted to is not active (not provisioned, or suspended). User won't be created." % (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, 
											u"Cannot create auxiliary users for client without subscriptions."
										)
								else:
									self.logger.info(u"Skip auxiliary user '%s' as it already exists in PPA", 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, u"Failed to create auxiliary roles of 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 
						)
					]

					self.logger.debug(u"Creating new 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, u"Failed to create auxiliary user role with Plesk API."):
							role = roles_by_names[role_name]
							self._create_auxiliary_role_with_plesk_api(client_guid, default_permissions, role)

					self.logger.debug(u"Updating roles pre-created by Plesk but not built-in")
					for role_name in roles_names & existing_non_built_in_roles_names:
						with self.safe.try_auxiliary_user_role(client.login, role_name, u"Failed to update auxiliary user role with Plesk API."):
							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, u"Failed to create auxiliary user role with Plesk API."):
									role = roles_by_names[role_name]
									self._create_auxiliary_role_with_plesk_api(client_guid, default_permissions, role)
							else:
								self.logger.info(u"Skip auxiliary user role '%s' as it already exists in PPA", 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
