from parallels.core import messages

import os
import ntpath
import logging
import threading
import socket
import struct
import pickle
import ssl

from parallels.core.registry import Registry
from parallels.core.utils import download, download_zip
from parallels.core.utils.steps_profiler import sleep
from parallels.core.utils import get_thirdparties_base_path, get_agents_base_path
from parallels.core.utils import windows_utils
from parallels.core.utils import windows_thirdparty
from parallels.core.utils.paexec import RemoteNodeSettings, PAExecCommand

logger = logging.getLogger(__name__)


class WindowsAgentRemoteObject(object):
	def __init__(self, settings, migrator_server):
		self._settings = settings
		self._migrator_server = migrator_server
		self._sockets = {}
		self._started_automatically = False
		remote_node_settings = RemoteNodeSettings(
			remote_server_ip=self._settings.ip,
			username=self._settings.windows_auth.username,
			password=self._settings.windows_auth.password
		)
		self._paexec_cmd = PAExecCommand(migrator_server, remote_node_settings)

	def deploy_and_connect(self):
		logger.debug(
			messages.DEBUG_TRY_TO_CONNECT_TO_TRANSFER_AGENT,
			self._settings.ip, self._settings.agent_settings.port
		)
		if not self.try_connect():
			logger.debug(
				messages.CONNECTION_FAILED_MOST_LIKELY_THAT_PANEL)
			logger.info(messages.DEPLOY_AND_START_PANEL_MIGRATOR_TRANSFER, self._settings.ip)
			try:
				logger.debug(messages.TRY_START_PANEL_MIGRATOR_TRANSFER_AGENT)
				self.start()
			except Exception:
				logger.debug(
					messages.EXCEPTION_WHEN_TRYING_START_PANEL_MIGRATOR,
					exc_info=True
				)

				# failed to start - then deploy and start
				try:
					self._deploy_and_start()
				except Exception:
					logger.debug(
						messages.EXCEPTION_WHEN_TRYING_DEPLOY_AND_START,
						exc_info=True
					)
					self._print_agent_installation_instructions()
				else:
					if not self.try_connect_multiple():  # wait for agent to start
						self._print_agent_installation_instructions()
					else:
						self._started_automatically = True
			else:
				if not self.try_connect_multiple():
					# started successfully, but failed to connect - then redeploy and start
					try:
						self._deploy_and_start()
					except Exception:
						logger.debug(
							messages.EXCEPTION_WHEN_TRYING_DEPLOY_AND_START_1,
							exc_info=True
						)
						self._print_agent_installation_instructions()
					else:
						if not self.try_connect_multiple():  # wait for agent to start
							self._print_agent_installation_instructions()
						else:
							self._started_automatically = True
				else:
					self._started_automatically = True

	def _deploy_and_start(self):
		logger.debug(
			messages.TRY_STOP_PANEL_MIGRATOR_TRANSFER_AGENT)
		try:
			self.stop(force=True)
		except Exception:
			logger.debug(
				messages.EXCEPTION_WHEN_TRYING_STOP_PANEL_MIGRATOR,
				exc_info=True
			)
		logger.debug(messages.DEPLOY_PANEL_MIGRATOR_TRANSFER_AGENT_AT, self._settings.ip)
		self.deploy()
		logger.debug(messages.START_PANEL_MIGRATOR_TRANSFER_AGENT_AT, self._settings.ip)
		self.start()

	def _print_agent_installation_instructions(self):
		raise Exception(
			messages.WINDOWS_AGENT_INSTALL_INSTRUCTIONS.format(
				source_ip=self._settings.ip,
				dist="%s\\panel-migrator-transfer-agent-installer.exe" % get_thirdparties_base_path(),
				port=self._settings.agent_settings.port,
				windows_agent_dir=self._settings.agent_settings.agent_path
			)
		)

	def deploy(self):
		# import there to avoid dependency on OpenSSL on Linux
		from parallels.core.utils.ssl_keys import SSLKeys

		ssl_keys = SSLKeys(self._migrator_server)

		python_path = os.path.join(get_thirdparties_base_path(), 'python')
		if not os.path.exists(python_path):
			storage_url = 'http://autoinstall.plesk.com/panel-migrator/thirdparties/python.zip'
			logger.info(messages.DOWNLOAD_PYTHON.format(url=storage_url))
			download_zip(storage_url, python_path)

		seven_zip_path = os.path.join(get_thirdparties_base_path(), '7zip')
		if not os.path.exists(seven_zip_path):
			storage_url = 'http://autoinstall.plesk.com/panel-migrator/thirdparties/7zip.zip'
			logger.info(messages.DOWNLOAD_7ZIP.format(url=storage_url))
			download_zip(storage_url, seven_zip_path)

		transfer_agent_path = os.path.join(get_agents_base_path(), 'transfer-agent')
		if not os.path.exists(transfer_agent_path):
			storage_url = 'http://autoinstall.plesk.com/panel-migrator/thirdparties/transfer-agent.zip'
			logger.info(messages.DOWNLOAD_TRANSFER_AGENT.format(url=storage_url))
			download_zip(storage_url, transfer_agent_path)

		paexec_path = os.path.join(get_thirdparties_base_path(), 'paexec.exe')
		if not os.path.exists(paexec_path):
			storage_url = 'http://autoinstall.plesk.com/panel-migrator/thirdparties/paexec.exe'
			logger.info(messages.DOWNLOAD_PAEXEC.format(url=storage_url))
			download(storage_url, paexec_path)

		transfer_agent_installer_path = ur'{thirdparties_dir}\panel-migrator-transfer-agent-installer.exe'.format(
			thirdparties_dir=get_thirdparties_base_path()
		)

		if os.path.exists(transfer_agent_installer_path):
			os.remove(transfer_agent_installer_path)

		logger.debug(messages.PACK_AGENT_INTO_SELFEXTRACTABLE_ARCHIVE)
		files_to_pack = [
			r'"{transfer_agent_installer_path}"',
			r'"{python_path}"',
			r'"{thirdparties_dir}\paexec.exe"',
			r'"{transfer_agent_dir}\server.py"',
			r'"{transfer_agent_dir}\config.ini"',
			r'"{transfer_agent_dir}\logging.config"',
			r'"{transfer_agent_dir}\start.bat"',
			r'"%s"' % ssl_keys.source_node_key_filename,
			r'"%s"' % ssl_keys.source_node_crt_filename,
			r'"%s"' % ssl_keys.migration_node_crt_filename,
		]
		with self._migrator_server.runner() as runner:
			runner.sh(windows_utils.cmd_command(
				(r'"{thirdparties_dir}\7zip\7za.exe" a -sfx ' + " ".join(files_to_pack)).format(
					transfer_agent_installer_path=transfer_agent_installer_path,
					python_path=python_path,
					thirdparties_dir=get_thirdparties_base_path(),
					transfer_agent_dir=windows_thirdparty.get_transfer_agent_dir(),
					files=" ".join(files_to_pack)
				)
			))

		logger.debug(messages.DEBUG_UPLOAD_AND_INSTALL_AGENT)
		self._paexec_cmd.run(
			executable=r".\panel-migrator-transfer-agent-installer.exe",
			# unpack archive to self._remote_agent_dir
			arguments='-o%s -y' % self._settings.agent_settings.agent_path,
			# first, deploy panel-migrator-transfer-agent-installer.exe to the server
			copy_program=True
		)
		# change python name to make it distinguishable from other python executables server
		remote_python_path = os.path.join(self._settings.agent_settings.agent_path, 'python')
		self._paexec_cmd.run(
			executable=r"cmd.exe",
			arguments='/C move %s %s' % (
				os.path.join(remote_python_path, 'python.exe'),
				os.path.join(remote_python_path, 'panel-migrator-python.exe')
			),
		)

	def start(self):
		self._paexec_cmd.run(
			executable=ntpath.join(self._settings.agent_settings.agent_path, "start.bat"),
			# do not wait for start script to finish
			do_not_wait=True,
		)

	def stop(self, force=False):
		# Shutdown agent only if it was started automatically.
		# In case agent was deployed/started by customer - don't stop it.
		if self._started_automatically or force:
			# terminate rsync server process at the same host because it could be runned earlier
			# by previously used transfer agent (for example, in scope of another
			# migration session), hang up and block tranfer agent port
			self._paexec_cmd.run(
				executable="cmd.exe",
				arguments="/c taskkill /F /IM panel-migrator-rsync.exe",
				is_unchecked=True
			)
			# remove just terminated rsync sever from pool
			Registry.get_instance().get_context().rsync_pool.stop(self._settings.ip, True)
			# terminate transfer agent process
			self._paexec_cmd.run(
				executable="cmd.exe",
				arguments="/c taskkill /F /IM panel-migrator-python.exe",
				is_unchecked=True
			)

	def shutdown(self):
		for sock in self._sockets.itervalues():
			sock.close()
		self.stop()

	def try_connect_multiple(self, attempts=20, interval=1):
		for _ in xrange(attempts):
			if self.try_connect():
				return True
			sleep(interval, messages.TRY_CONNECT_AGENT_AT_S % self._settings.ip)
		return False

	def try_connect(self):
		try:
			self.connect()
			return True
		except socket.error:
			logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			return False

	def connect(self):
		# import there to avoid dependency on OpenSSL on Linux
		from parallels.core.utils.ssl_keys import SSLKeys

		sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		if self._settings.agent_settings.use_ssl:
			logger.debug(messages.SSL_IS_ENABLED_FOR_AGENT_AT, self._settings.ip)

			client_key = self._settings.agent_settings.client_key
			client_cert = self._settings.agent_settings.client_cert
			server_cert = self._settings.agent_settings.server_cert

			ssl_files = [client_key, client_cert, server_cert]

			if any([f is None for f in ssl_files]):
				ssl_keys = SSLKeys(self._migrator_server)
				if client_key is None:
					client_key = ssl_keys.migration_node_key_filename
				if client_cert is None:
					client_cert = ssl_keys.migration_node_crt_filename
				if server_cert is None:
					server_cert = ssl_keys.source_node_crt_filename

			logger.debug(messages.DEBUG_SSL_CLIENT_KEY, client_key)
			logger.debug(messages.DEBUG_SSL_CLIENT_CERT, client_cert)
			logger.debug(messages.DEBUG_SSL_SERVER_CERT, server_cert)

			sock = ssl.wrap_socket(
				sock,
				keyfile=client_key,
				certfile=client_cert,
				cert_reqs=ssl.CERT_REQUIRED,
				ca_certs=server_cert
			)
		else:
			logger.debug(messages.SSL_IS_DISABLED_FOR_AGENT_AT, self._settings.ip)

		logger.debug(messages.DEBUG_CONNECT_TO_AGENT, self._settings.ip)
		sock.connect((
			self._settings.ip, self._settings.agent_settings.port
		))
		self.set_socket(sock)

	def reconnect(self):
		logger.debug(messages.DEBUG_RECONNECT_TO_AGENT, self._settings.ip)
		# close socket
		sock = self.get_socket(autoconnect=False)
		if sock is not None:
			try:
				sock.close()
			except:
				logger.debug(messages.EXCEPTION_WHEN_CLOSING_SOCKET, exc_info=True)
		# try to connect
		self.connect()

	def __getattr__(self, attr):
		remote_function_name = attr

		def run_remote_function(*args, **kwargs):
			def run():
				command = pickle.dumps(
					(remote_function_name, args, kwargs)
				)
				self.get_socket().sendall(struct.pack("I", len(command)))
				self.get_socket().sendall(command)
				length = self._receive(4)
				length, = struct.unpack("I", length)
				if length == 0:
					raise Exception(
						messages.WINDOWS_FAILED_TO_EXECUTE_REMOTE_COMMAND.format(
							server_ip=self._settings.ip,
							debug_log_path=ntpath.join(self._settings.agent_settings.agent_path, 'debug.log')
						)
					)
				else:
					result = pickle.loads(self._receive(length))
					return result

			return self._run_multiple_attempts(run)

		return run_remote_function

	def _run_multiple_attempts(self, run_function, max_attempts=5, interval_between_attempts=10):
		for attempt in range(0, max_attempts):
			try:
				if attempt > 0:
					# reconnect on 2nd and later attempts
					self.reconnect()

				result = run_function()
				if attempt > 0:
					logger.info(
						messages.REMOTE_OPERATION_OK_AFTER_ATTEMPTS.format(
							host=self._settings.ip, attempts=attempt + 1
						)
					)
				return result
			except socket.error as e:
				logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				if attempt >= max_attempts - 1:
					raise e
				else:
					logger.error(
						messages.REMOTE_OPERATION_FAILED_RETRY.format(
							interval_between_attempts=interval_between_attempts
						)
					)
					sleep(interval_between_attempts, messages.SLEEP_RETRY_RUNNING_REMOTE_COMMAND)

	def _receive(self, size):
		b = ''
		while len(b) < size:
			r = self.get_socket().recv(size - len(b))
			if not r:
				return b
			b += r
		return b

	def get_socket(self, autoconnect=True):
		if threading.current_thread() not in self._sockets:
			if autoconnect:
				self.connect()
			else:
				return None
		return self._sockets[threading.current_thread()]

	def set_socket(self, sock):
		self._sockets[threading.current_thread()] = sock
