from parallels.common import messages
import logging
import threading
import socket
import struct
import pickle
import ssl

from parallels.common.utils.steps_profiler import sleep
from parallels.common.utils import windows_utils
from parallels.common.utils import windows_thirdparty
from parallels.common.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)
		self._remote_agent_dir = r'C:\panel-migrator-transfer-agent'

	def deploy_and_connect(self):
		logger.debug(
			"Try to connect to panel migrator transfer agent at %s:%s",
			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(
			u"Panel Migrator failed to install Transfer Agent to '{source_ip}' server. \nTransfer Agent is required for communication between current server and '{source_ip}' server\n\nFirst, check that Windows Administrator's credentials in config.ini are correct for the server\n\nIf they are correct, it is very likely that Samba is disabled on the remote server,\nso automatic installation of agent is not possible.\nTo install it manually:\n1) On the {source_ip} server create directory C:\\panel-migrator-transfer-agent\n2) Upload {dist} file to C:\\panel-migrator-transfer-agent of the {source_ip} server.\n3) Run panel-migrator-transfer-agent-installer.exe on the {source_ip} server.\n4) Open C:\\panel-migrator-transfer-agent directory in Windows Explorer.\n5) Run 'start.bat' script there\n\nAlso, check that there are no firewall rules blocking connections\nto {port} port on {source_ip} from that server.\n\nFor troubleshooting, refer to: \n1) debug.log and info.log files in C:\\panel-migrator-transfer-agent\ndirectory on {source_ip} server.\n2) debug.log and info.log files on the current server.".format(
				source_ip=self._settings.ip,
				dist="%s\\panel-migrator-transfer-agent-installer.exe" % windows_thirdparty.get_thirdparties_dir(),
				port=self._settings.agent_settings.port
			)
		)

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

		ssl_keys = SSLKeys(self._migrator_server)

		logger.debug(messages.PACK_AGENT_INTO_SELFEXTRACTABLE_ARCHIVE)
		files_to_pack = [
			r"{thirdparties_dir}\panel-migrator-transfer-agent-installer.exe",
			r"{thirdparties_dir}\python27",
			r"{transfer_agent_dir}\server.py",
			r"{transfer_agent_dir}\config.ini",
			r"{transfer_agent_dir}\logging.config",
			r"{transfer_agent_dir}\start.bat",
			"%s" % ssl_keys.source_node_key_filename,
			"%s" % ssl_keys.source_node_crt_filename,
			"%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(
					thirdparties_dir=windows_thirdparty.get_thirdparties_dir(),
					transfer_agent_dir=windows_thirdparty.get_transfer_agent_dir(),
					files=" ".join(files_to_pack)
				)
			))

		logger.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._remote_agent_dir,
			# first, deploy panel-migrator-transfer-agent-installer.exe to the server
			copy_program=True
		)

	def start(self):
		self._paexec_cmd.run(
			executable=r"C:\panel-migrator-transfer-agent\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:
			self._paexec_cmd.run(
				executable="cmd.exe",
				arguments="/c taskkill /F /IM panel-migrator-python.exe"
			)

	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("Exception: ", exc_info=True)
			return False

	def connect(self):
		# import there to avoid dependency on OpenSSL on Linux
		from parallels.common.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("SSL client key: %s", client_key)
			logger.debug("SSL client certificate: %s", client_cert)
			logger.debug("SSL server certificate: %s", 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("Connect to agent at '%s'", self._settings.ip)
		sock.connect((
			self._settings.ip, self._settings.agent_settings.port
		))
		self.set_socket(sock)

	def reconnect(self):
		logger.debug("Reconnect to agent at '%s'", 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(
						"Failed to execute remote command on '%s' server. Check debug log at C:\\panel-migrator-transfer-agent\\debug.log for more details" % (self._settings.ip)
					)
				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(
						'Remote operation executed successfully at {host} after {attempts} attempt(s).'.format(
							host=self._settings.ip, attempts=attempt+1
						)
					)
				return result
			except socket.error as e:
				logger.debug("Exception: ", exc_info=True)
				if attempt >= max_attempts - 1:
					raise e
				else:
					logger.error(
						'Failed to run remote operation, retry in {interval_between_attempts} seconds'.format(
							interval_between_attempts=interval_between_attempts
						)
					)
					sleep(interval_between_attempts, '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
