import logging
import os
from collections import namedtuple
from contextlib import contextmanager

from parallels.common import MigrationError
from parallels.common.utils import windows_utils
from parallels.common.utils.steps_profiler import sleep
from parallels.utils import generate_random_password
from parallels.common.utils import windows_thirdparty
from parallels.common.utils.windows_utils import cmd_command
from parallels.common.utils.windows_utils import file_exists

logger = logging.getLogger(__name__)

RsyncConfig = namedtuple('RsyncConfig', (
	'client_ips', 'vhosts_dir', 'migrator_dir', 'user', 'password'
))

class RsyncInstaller(object):
	"""Install rsync on remote Windows node"""

	def __init__(self, server):
		self.server = server

	def install(self):
		with self.server.runner() as runner:
			if file_exists(
				runner, self.server.get_session_file_path(
					ur'rsync\bin\rsync.exe'
				)
			):
				logger.debug(
					u"Rsync is already installed on server, skip installation."
					u"If you want to reinstall it, stop it with stop.bat script, "
					u"remove '%s' directory and then restart migration", 
					self.server.get_session_file_path('rsync')
				)
				return

			installer_path = self.server.get_session_file_path(
				'rsync-installer.exe'
			)
			runner.upload_file(os.path.join(
				windows_thirdparty.get_thirdparty_dir(),
				"rsync-installer.exe",
			), installer_path)
			runner.sh(cmd_command(
				r'%s -o%s -y' % (
					installer_path, 
					self.server.session_dir()
				)
			))

class RsyncInstallerPAExec(object):
	"""Install rsync on remote Windows node with paexec

	As PAExecRunner uses rsync to upload rsync installer,
	we can't use that runner with RsyncServerInstaller class
	as rsync is not installed yet on the node. However,
	we can do it with plain paexec call when program is 
	uploaded and then executed on remote node. So this class requires 
	credentials to source Windows node, not runner.
	"""

	def __init__(self, local_server, server):
		self.local_server = local_server
		self.server = server

	def install(self):
		with self.server.runner() as runner:
			if file_exists(
				runner, 
				self.server.get_session_file_path(ur'rsync\bin\rsync.exe')
			):
				logger.debug(
					u"Rsync is already installed on server, skip installation. "
					u"If you want to reinstall it, stop it with stop.bat script, "
					u"remove '%s' directory and then restart migration", 
					self.server.get_session_file_path('rsync')
				)
				return

			passwords_file = self.local_server.get_session_file_path(
				'node-password-%s' % runner.settings.ip
			)
			with open(passwords_file, 'wb') as f:
				f.write(runner.settings.windows_auth.password)
			with self.local_server.runner() as local_runner:
				local_runner.sh(cmd_command(
					r'cd %s &&%s \\%s -u %s -p@ %s -p@d -c %s -o%s -y' % (
						windows_thirdparty.get_thirdparty_dir(),
						windows_thirdparty.get_paexec_bin(),
						runner.settings.ip,
						runner.settings.windows_auth.username,
						os.path.abspath(passwords_file),
						".\\rsync-installer.exe",
						self.server.session_dir()
					)
				))

class RsyncServer(object):
	"""Configure and control rsync server"""

	def __init__(self, rsync_dir):
		"""Object constructor
		
		Arguments:
		- rsync_dir - directory with rsync binary and control bat scripts
		"""
		self.rsync_dir = rsync_dir

	def configure(self, server_node_runner, rsync_config):
		"""Configure rsync server (rsyncd.conf and rsyncd.secrets)"""
		server_node_runner.sh(cmd_command(
			r'cd "{rsync_dir}"&&configure.bat '
			r'{source_servers} {vhosts_dir} {migrator_dir} '
			r'{user} {password}'.format(
				rsync_dir=self.rsync_dir,
				source_servers=rsync_config.client_ips,
				vhosts_dir=windows_utils.convert_path_to_cygwin(
					rsync_config.vhosts_dir
				) if rsync_config.vhosts_dir is not None else '/cygdrive/c', # XXX
				migrator_dir=windows_utils.convert_path_to_cygwin(
					rsync_config.migrator_dir
				),
				user=rsync_config.user,
				password=rsync_config.password,
			)
		))

	def restart(self, server_node_runner):
		"""Stop rsync if it is running, then start"""
		server_node_runner.sh(cmd_command(
			r'cd "{rsync_dir}"&&restart.bat'.format(
				rsync_dir=self.rsync_dir,
			)
		))

	def stop(self, server_node_runner):
		"""Stop rsync server"""
		server_node_runner.sh(cmd_command(
			r'cd "{rsync_dir}"&&stop.bat'.format(
				rsync_dir=self.rsync_dir,
			)
		))

@contextmanager
def windows_rsync(
	source_server, target_server, vhosts_dir=None, 
	rsync_installer_source=None, rsync_installer_target=None
):
	"""
	Configure rsync connections between 2 nodes

	Returns RsyncControl object.

	Arguments:
	- source_server - server where you want to set up rsync server, download
	  files from, upload files to
	- target server - server where you want to set up rsync client, download
	  files to, upload files from
	- vhosts_dir - directory with content of virtual hosts, if None is
	  specified, no rsync path is configured for it
	- rsync_installer - class that can install rsync client and server
	"""
	# Install rsync on source
	logger.debug("Install rsync on source server")
	if rsync_installer_source is None:
		rsync_installer_source = RsyncInstaller(source_server)
	rsync_installer_source.install()

	# Install rsync on target
	logger.debug("Install rsync on target server")
	if rsync_installer_target is None:
		rsync_installer_target = RsyncInstaller(target_server)
	rsync_installer_target.install()

	rsync_server = RsyncServer(
		source_server.get_session_file_path(ur'rsync\bin')
	)
	with source_server.runner() as runner:
		# Configure and start rsync server on source
		login = 'admin'
		password = generate_random_password()

		logger.debug("Configure rsync on source server")
		rsync_server.configure(
			runner,
			RsyncConfig(
				# Always allow connections from all IPs to avoid problems with
				# routing and NAT, when incoming IP is not the same as any
				# host's IP. Get connection secure only by password
				# authentication.
				client_ips='"*"',
				vhosts_dir=vhosts_dir,
				migrator_dir=source_server.session_dir(),
				user=login,
				password=password
			)
		)
		try:
			rsync_server.restart(runner)
			with target_server.runner() as target_runner:
				# Yield with rsync client
				yield RsyncControl(
					target_runner=target_runner,
					target_rsync_bin=target_server.get_session_file_path(
						r'rsync\bin\rsync.exe'
					),
					source_ip=source_server.ip(),
					source_login=login,
					source_password=password
				)
		finally:
			rsync_server.stop(runner)

@contextmanager
def windows_rsync_preconfigured_source(target_server, source_ip):
	"""Configure rsync for connection to preconfigured rsync server
	
	Returns RsyncControl object.
	"""
	# Install rsync on target
	rsync_installer_target = RsyncInstaller(target_server)
	rsync_installer_target.install()

	with target_server.runner() as target_runner:
		# Yield with rsync client
		yield RsyncControl(
			target_runner=target_runner,
			target_rsync_bin=target_server.get_session_file_path(
				r'rsync\bin\rsync.exe'
			),
			source_ip=source_ip,
			# consider that there is no login and password required
			# fot the preconfigured rsync server
			source_login=None, 
			source_password=None
		)

class RsyncControl(object):
	"""Simple object to work with provided rsync client and rsync server"""
	def __init__(self, target_runner, target_rsync_bin, source_ip, source_login, source_password):
		self.target_rsync_bin = target_rsync_bin
		self.target_runner = target_runner
		self.source_ip = source_ip
		self.source_login = source_login
		self.source_password = source_password
		self.max_attempts = 5
		self.interval_between_attempts = 10

	def sync(self, source_path, target_path, exclude=None):
		if self.source_password is not None:
			set_password = u'set RSYNC_PASSWORD=%s&&' % (self.source_password,)
		else:
			set_password = u''

		if self.source_login is not None:
			login_clause = '%s@' % (self.source_login,) 
		else:
			login_clause = ''

		cmd = cmd_command(
			set_password + \
			u"{rsync_bin} "
			u"--no-perms -t -r "
			u"rsync://%s{source_ip}/{source_path} {target_path}" % (
				login_clause,
			)
		)
		if exclude is not None and len(exclude) > 0:
			cmd += u"".join([u" --exclude %s" % ex for ex in exclude])
		args = dict(
			rsync_bin=self.target_rsync_bin,
			source_ip=self.source_ip,
			source_path=source_path,
			target_path=target_path,
		)

		for attempt in range(0, self.max_attempts):
			exit_code, stdout, stderr = self.target_runner.sh_unchecked(cmd, args)

			if exit_code == 0:
				if attempt > 0:
					logger.info(
						'Copy files from {source_ip} was finished '
						'successfully after {attempts} attempt(s).'.format(
							source_ip=self.source_ip, attempts=attempt+1
						)
					)
				return stdout
			else:
				if attempt >= self.max_attempts - 1:
					raise RsyncClientNonZeroExitCode(
						exit_code, stdout, stderr, cmd.format(**args)
					)
				else:
					logger.error(
						'Failed to copy files with rsync, '
						'retry in {interval_between_attempts} seconds'.format(
							interval_between_attempts=\
								self.interval_between_attempts
						)
					)
					sleep(self.interval_between_attempts, 'Retry rsync command')

	def upload(self, source_path, target_path):
		if self.source_password is not None:
			set_password = u'set RSYNC_PASSWORD=%s&&' % (self.source_password,)
		else:
			set_password = u''

		if self.source_login is not None:
			login_clause = '%s@' % (self.source_login,) 
		else:
			login_clause = ''

		cmd = cmd_command(
			set_password + \
			u"{rsync_bin} "
			u"--no-perms -t -r "
			u"{source_path} rsync://%s{source_ip}/{target_path}" % (
				login_clause,
			)
		)
		args = dict(
			rsync_bin=self.target_rsync_bin,
			source_ip=self.source_ip,
			source_path=source_path,
			target_path=target_path,
		)

		for attempt in range(0, self.max_attempts):
			exit_code, stdout, stderr = self.target_runner.sh_unchecked(cmd, args)

			if exit_code == 0:
				if attempt > 0:
					logger.info(
						'Copy files to {source_ip} was finished '
						'successfully after {attempts} attempt(s).'.format(
							source_ip=self.source_ip, attempts=attempt+1
						)
					)
				return stdout
			else:
				if attempt >= self.max_attempts - 1:
					raise RsyncClientNonZeroExitCode(
						exit_code, stdout, stderr, cmd.format(**args)
					)
				else:
					logger.error(
						'Failed to copy files with rsync, '
						'retry in {interval_between_attempts} seconds'.format(
							interval_between_attempts=\
								self.interval_between_attempts
						)
					)
					sleep(self.interval_between_attempts, 'Retry rsync command')

	def list_files(self, source_path):
		if self.source_password is not None:
			set_password = u'set RSYNC_PASSWORD=%s&&' % (self.source_password,)
		else:
			set_password = u''

		if self.source_login is not None:
			login_clause = '%s@' % (self.source_login,) 
		else:
			login_clause = ''

		cmd = cmd_command((
			u"{set_password}{rsync_binary_path} -r --list-only "
			"rsync://{login_clause}{source_ip}/{source_path}"
		).format(
			set_password=set_password,
			rsync_binary_path=self.target_rsync_bin,
			login_clause=login_clause,
			source_ip=self.source_ip,
			source_path=source_path,
		))
		exit_code, stdout, stderr = self.target_runner.sh_unchecked(cmd)
		if exit_code != 0:
			raise RsyncClientNonZeroExitCode(
				exit_code, stdout, stderr, cmd
			)
		return stdout

class RsyncClientNonZeroExitCode(MigrationError):
	def __init__(self, exit_code, stdout, stderr, command):
		self.exit_code = exit_code
		self.stdout = stdout
		self.stderr = stderr
		self.command = command

