import logging
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._socket = None
		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(
				"Connection failed, most likely that panel migrator transfer agent is not installed or running."
			)
			logger.info("Deploy and start panel migrator transfer agent at '%s'", self._settings.ip)
			try:
				logger.debug("Try to start panel migrator transfer agent. If it was not deployed, this will fail")
				self.start()
			except Exception:
				logger.debug(
					"Exception when trying to start panel migrator transfer agent, "
					"most likely agent was not deployed, and the exception could be ignored: ",
					exc_info=True
				)

				# failed to start - then deploy and start
				try:
					self._deploy_and_start()
				except Exception:
					logger.debug(
						"Exception when trying to deploy and start panel migrator transfer agent, "
						"most likely agent failed to deploy, print manual deploy instructions. Exception: ",
						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(
							"Exception when trying to deploy and start panel migrator transfer agent, "
							"most likely agent failed to deploy, print manual deploy instructions. Exception: ",
							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(
			"Try to stop panel migrator transfer agent if it is already running, but is broken for some reason, "
			"or has different SSL keys"
		)
		try:
			self.stop(force=True)
		except Exception:
			logger.debug(
				"Exception when trying to stop panel migrator transfer agent, most likely it could be ignored: ",
				exc_info=True
			)
		logger.debug("Deploy panel migrator transfer agent at '%s'", self._settings.ip)
		self.deploy()
		logger.debug("Start panel migrator transfer agent at '%s'", 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. \n"
			u"Transfer Agent is required for communication between current server and '{source_ip}' server\n"
			u"\n"
			u"First, check that Windows Administrator's credentials in config.ini are correct for the server\n"
			u"\n"
			u"If they are correct, it is very likely that Samba is disabled on the remote server,\n"
			u"so automatic installation of agent is not possible.\n"
			u"To install it manually:\n"
			u"1) On the {source_ip} server create directory C:\panel-migrator-transfer-agent\n"
			u"2) Upload {dist} file to C:\panel-migrator-transfer-agent of the {source_ip} server.\n"
			u"3) Run panel-migrator-transfer-agent-installer.exe on the {source_ip} server.\n"
			u"4) Open C:\panel-migrator-transfer-agent directory in Windows Explorer.\n"
			u"5) Run 'start.bat' script there\n"
			u"\n"
			u"Also, check that there are no firewall rules blocking connections\n"
			u"to {port} port on {source_ip} from that server.\n"
			u"\n"
			u"For troubleshooting, refer to: \n"
			u"1) debug.log and info.log files in C:\panel-migrator-transfer-agent\n"
			u"directory on {source_ip} server.\n"
			u"2) 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("Pack agent into self-extractable 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):
		self._socket.close()
		self.stop()

	def try_connect_multiple(self, attempts=20, interval=1):
		for _ in xrange(attempts):
			if self.try_connect():
				return True
			sleep(interval, "Try to connect to 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

		self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		if self._settings.agent_settings.use_ssl:
			logger.debug("SSL is enabled for agent at '%s'", 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)

			self._socket = ssl.wrap_socket(
				self._socket,
				keyfile=client_key,
				certfile=client_cert,
				cert_reqs=ssl.CERT_REQUIRED,
				ca_certs=server_cert
			)
		else:
			logger.debug("SSL is disabled for agent at '%s'", self._settings.ip)

		logger.debug("Connect to agent at '%s'", self._settings.ip)
		self._socket.connect((
			self._settings.ip, self._settings.agent_settings.port
		))

	def reconnect(self):
		logger.debug("Reconnect to agent at '%s'", self._settings.ip)
		# close socket
		if self._socket is not None:
			try:
				self._socket.close()
			except:
				logger.debug("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._socket.sendall(struct.pack("I", len(command)))
				self._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._socket.recv(size - len(b))
			if not r:
				return b
			b += r
		return b
