from parallels.core import messages
from collections import namedtuple
from textwrap import dedent

from parallels.core.utils.config_utils import \
	ConfigSection, read_http_connection_settings, read_auth, \
	HttpConnectionSettings
from parallels.core.utils.common import obj, format_list, is_run_on_windows, default
from parallels.core.utils.common import is_empty
from parallels.core.utils.common.ip import is_ipv6
from . import MigrationError, MigrationConfigurationFileError


MailContent = obj(
	NONE='none',
	MESSAGES='messages',
	FULL='full',
)

MailImapEncryption = obj(
	NONE='none',
	SSL='ssl',
	TLS='tls'
)


class ExternalDBConfig(namedtuple('ExternalDBConfig', ('id', 'host', 'ip', 'ssh_auth'))):
	@property
	def is_windows(self):
		return False

CopyMailContentSettings = namedtuple('CopyMailContentSettings', (
	# value of MailContent 
	'mode',
	# value of MailImapEncryption
	'mail_imap_encryption', 
	# custom command as a string - see common.mail for details
	'custom_mail_copy_content_command',
	# imapsync option - see common.mail.imapsync for details
	'mailbox_name_separator',
	# imapsync option - see common.mail.imapsync for details
	'mailbox_name_prefix'
))


def read_copy_mail_content_settings(section, is_windows):
	if is_windows:
		cmc_allowed_values = [
			MailContent.FULL, MailContent.MESSAGES, MailContent.NONE
		]
	else:
		cmc_allowed_values = [
			MailContent.FULL, MailContent.NONE
		]

	cmc = section.get('copy-mail-content', MailContent.FULL)
	copy_mail_content = cmc.lower()

	if copy_mail_content not in cmc_allowed_values:
		raise MigrationError(
			u"Invalid value '%s' is specified for 'copy-mail-content' setting in '%s' section of tool's configuration file, should be one of: %s (case-insensitive)." % (
				copy_mail_content, section, ",".join(cmc_allowed_values)
			)
		)

	custom_mail_copy_content_command = section.get(
		'custom-mail-copy-content-command', None
	)

	mail_imap_encryption_allowed_values = [
		MailImapEncryption.NONE, MailImapEncryption.SSL, MailImapEncryption.TLS
	]
	mail_imap_encryption = section.get(
		'imap-encryption', MailImapEncryption.NONE
	).lower()
	if mail_imap_encryption not in mail_imap_encryption_allowed_values:
		raise MigrationError(
			u"Invalid value '%s' is specified for 'imap-encryption' setting in '%s' section of tool's configuration file, should be one of: %s (case-insensitive)." % (
				mail_imap_encryption, section.section_name, 
				", ".join(mail_imap_encryption_allowed_values)
			)
		)

	mailbox_name_prefix = section.get('mailbox-name-prefix', None)
	mailbox_name_separator = section.get('mailbox-name-separator', None)

	return CopyMailContentSettings(
		mode=copy_mail_content, mail_imap_encryption=mail_imap_encryption,
		custom_mail_copy_content_command=custom_mail_copy_content_command,
		mailbox_name_separator=mailbox_name_separator,
		mailbox_name_prefix=mailbox_name_prefix,
	)


WindowsAgentSettings = namedtuple('WindowsAgentSettings', (
	'enabled', 'use_ssl', 'port', 'client_cert', 'client_key', 'server_cert')
)


class SourceUnixConfig(object):
	def __init__(self, id, ip, ssh_auth, session_dir, mail_settings):
		self._id = id
		self._ip = ip
		self._ssh_auth = ssh_auth
		self._session_dir = session_dir
		self._mail_settings = mail_settings

	@property
	def id(self):
		return self._id

	@property
	def ip(self):
		return self._ip

	@property
	def ssh_auth(self):
		return self._ssh_auth

	@property
	def is_windows(self):
		return False

	@property
	def session_dir(self):
		return self._session_dir

	@property
	def mail_settings(self):
		return self._mail_settings


class SourcePleskUnixConfig(namedtuple('SourcePleskUnixConfig', (
	'id', 'ip', 'ssh_auth', 'plesk_api', 'session_dir', 'mail_settings',
))):
	@property
	def is_windows(self):
		return False


class SourcePleskWindowsConfig(namedtuple('SourcePleskWindowsConfig', (
	'id', 'ip', 'windows_auth', 'plesk_api', 'session_dir', 'mail_settings', 'agent_settings'
))):
	@property
	def is_windows(self):
		return True


class TargetPleskConfigBase(object):
	def __init__(self, ip, plesk_api, unix_session_dir, windows_session_dir, cligate_ip=None):
		self._ip = ip
		self._cligate_ip = cligate_ip
		self._plesk_api = plesk_api
		self._unix_session_dir = unix_session_dir
		self._windows_session_dir = windows_session_dir

	@property
	def ip(self):
		return self._ip

	@property
	def cligate_ip(self):
		return default(self._cligate_ip, self.ip)

	@property
	def plesk_api(self):
		return self._plesk_api

	@property
	def unix_session_dir(self):
		return self._unix_session_dir

	@property
	def windows_session_dir(self):
		return self._windows_session_dir

	@property
	def is_windows(self):
		raise NotImplementedError()

	@property
	def is_local(self):
		raise NotImplementedError()


class TargetPleskUnixConfig(TargetPleskConfigBase):
	def __init__(self, ip, ssh_auth, plesk_api, unix_session_dir, windows_session_dir, cligate_ip=None):
		super(TargetPleskUnixConfig, self).__init__(
			ip, plesk_api, unix_session_dir, windows_session_dir, cligate_ip
		)
		self._ssh_auth = ssh_auth

	@property
	def ssh_auth(self):
		return self._ssh_auth

	@property
	def is_windows(self): 
		return False

	@property
	def is_local(self):
		"""If target server is local or remote
		
		Local server means that migrator works on the target node, remote means
		that migrator's node and target node are different servers
		"""
		# always false, local execution on Unix target node is not supported
		# yet, we always execute commands by SSH
		return self.ssh_auth is None


class TargetPleskWindowsConfig(TargetPleskConfigBase):
	def __init__(
		self, ip, windows_auth, plesk_api, unix_session_dir, windows_session_dir,
		agent_settings, cligate_ip=None
	):
		super(TargetPleskWindowsConfig, self).__init__(
			ip, plesk_api, unix_session_dir, windows_session_dir, cligate_ip
		)
		self._windows_auth = windows_auth
		self._agent_settings = agent_settings

	@property
	def windows_auth(self):
		return self._windows_auth

	@property
	def agent_settings(self):
		return self._agent_settings

	@property
	def is_windows(self):
		return True

	@property
	def is_local(self):
		"""If target server is local or remote
		
		Local server means that migrator works on the target node, remote means
		that migrator's node and target node are different servers
		"""
		return self.windows_auth.username is None


class TargetPPAConfig(TargetPleskUnixConfig):
	def __init__(self, ip, ssh_auth, poa_api, plesk_api, unix_session_dir, windows_session_dir, cligate_ip=None):
		super(TargetPPAConfig, self).__init__(
			ip, ssh_auth, plesk_api, unix_session_dir, windows_session_dir, cligate_ip
		)
		self._poa_api = poa_api

	@property
	def poa_api(self):
		return self._poa_api


POA_URL_DEFAULTS = dict(
	protocol='https',
	port=8440,
	path='/RPC2'
)

PLESK_URL_DEFAULTS = dict(
	protocol='https',
	port=8443,
	path='/enterprise/control/agent.php'
)


def read_windows_agent_settings(config, section_name):
	section = ConfigSection(config, section_name)
	return WindowsAgentSettings(
		enabled=section.get(
			'windows-agent-enabled',
			True if is_run_on_windows() else False
		),
		port=section.get('windows-agent-port', 10155),
		use_ssl=section.get('windows-agent-use-ssl', True),
		client_cert=section.get('windows-agent-client-cert', None), 
		client_key=section.get('windows-agent-client-key', None), 
		server_cert=section.get('windows-agent-server-cert', None)
	)


def read_plesk_connection_settings(config, section_name):
	"""Read Plesk API connection settings.
	"""
	section = ConfigSection(config, section_name)

	ip = section['ip']

	api_defaults = dict(
		PLESK_URL_DEFAULTS,
		host=ip,
		username=section['panel-username'],
		password=section['panel-password'],
	)
	return read_http_connection_settings(
		section.prefixed('plesk-').with_defaults(api_defaults)
	)


def read_source_dns_servers_settings(config, section_name):
	"""Read centralized dns ip addresses and credentials.
	"""
	section = ConfigSection(config, section_name)

	ip = section['ip']
	ssh_auth = read_ssh_auth(section)
	return SourcePleskUnixConfig(section_name, ip, ssh_auth, "false", "false", None)


def read_source_unix_server_settings(config, section_name):
	"""Read generic Unix server settings
	"""
	section = ConfigSection(config, section_name)
	ip = section['ip']
	ssh_auth = read_ssh_auth(section)
	session_dir = section.get('session-dir', '/tmp')
	mail_settings = read_copy_mail_content_settings(section, False)

	return SourceUnixConfig(section_name, ip, ssh_auth, session_dir, mail_settings)


def read_source_plesk_settings(config, section_name):
	"""Read Plesk API and SSH connection settings.
	"""
	section = ConfigSection(config, section_name)

	ip = section['ip']
	os = section['os']
	session_dir = section.get('session-dir', 'c:\\migrator' if os == 'windows' else '/tmp')

	is_windows = (os == 'windows')
	mail_settings = read_copy_mail_content_settings(section, is_windows)

	plesk_api = read_plesk_connection_settings(config, section_name)
	if os == 'windows':
		windows_auth = _read_windows_auth(section)
		return SourcePleskWindowsConfig(
			section_name, ip, windows_auth, plesk_api, session_dir,
			mail_settings, read_windows_agent_settings(config, section_name)
		)
	else:
		ssh_auth = read_ssh_auth(section)
		return SourcePleskUnixConfig(
			section_name, ip, ssh_auth, plesk_api, session_dir, mail_settings
		)



def read_target_plesk_settings(config, section_name):
	"""
	:rtype:
		parallels.core.migrator_config.TargetPleskWindowsConfig |
		parallels.core.migrator_config.TargetPleskUnixConfig
	"""
	section = ConfigSection(config, section_name)
	ip = section['ip']
	os = section['os']
	cligate_ip = section.get('cligate-ip')
	plesk_settings = dict(
		host=ip,
		username=config.get(section_name, 'panel-username'),
		password=config.get(section_name, 'panel-password'),
	)
	plesk_api = read_http_connection_settings(
		section.prefixed('plesk-').with_defaults(dict(PLESK_URL_DEFAULTS, **plesk_settings))
	)
	unix_session_dir = section.get('unix-session-dir', '/tmp')
	windows_session_dir = section.get('windows-session-dir', 'C:\\migrator')
	if os == 'windows':
		windows_auth = _read_windows_auth(section, required=False)
		return TargetPleskWindowsConfig(
			ip=ip, windows_auth=windows_auth, plesk_api=plesk_api,
			unix_session_dir=unix_session_dir,
			windows_session_dir=windows_session_dir,
			agent_settings=read_windows_agent_settings(config, section_name),
			cligate_ip=cligate_ip
		)
	else:
		ssh_auth = read_ssh_auth(section, required=False)
		return TargetPleskUnixConfig(
			ip=ip, ssh_auth=ssh_auth, plesk_api=plesk_api,
			unix_session_dir=unix_session_dir, windows_session_dir=windows_session_dir,
			cligate_ip=cligate_ip
		)



def read_ppa_settings(config, section_name):
	"""
	:rtype: parallels.core.migrator_config.TargetPPAConfig
	"""
	section = ConfigSection(config, section_name)

	ip = section['ip']
	if is_ipv6(ip):
		raise MigrationError(
			messages.PPA_API_DOES_NOT_WORK_OVER % ip
		)
	cligate_ip = section.get('cligate-ip')

	ssh_auth = read_ssh_auth(section, required=False)

	ppa_settings = dict(
		host=ip,
		username=config.get(section_name, 'panel-username'),
		password=config.get(section_name, 'panel-password'),
	)
	poa_api = _read_poa_connection_settings(
		section.prefixed('poa-').with_defaults(dict(POA_URL_DEFAULTS, **ppa_settings))
	)
	plesk_api = read_http_connection_settings(
		section.prefixed('plesk-').with_defaults(dict(PLESK_URL_DEFAULTS, **ppa_settings))
	)
	unix_session_dir = section.get('unix-session-dir', '/tmp')
	windows_session_dir = section.get('windows-session-dir', 'C:\\migrator')

	return TargetPPAConfig(
		ip=ip, ssh_auth=ssh_auth, poa_api=poa_api, plesk_api=plesk_api,
		unix_session_dir=unix_session_dir, windows_session_dir=windows_session_dir,
		cligate_ip=cligate_ip
	)



def read_external_db_settings(config, section_name):
	section = ConfigSection(config, section_name)
	host = section['host']
	ip = section['ip']
	ssh_auth = read_ssh_auth(section)
	return ExternalDBConfig(id=section_name, host=host, ip=ip, ssh_auth=ssh_auth)



class SSHAuthPassword(namedtuple('SSHAuthPassword', ('username', 'password'))):
	def connect(self, ip, client):
		"""
		ip - server IP address
		client - instance of paramiko.SSHClient
		"""
		client.connect(
			ip,
			username=self.username, password=self.password,
			look_for_keys=False
		)


class SSHAuthKeyFilename(namedtuple('SSHAuthKeyFilename', ('username', 'key_filename'))):
	def connect(self, ip, client):
		"""
		ip - server IP address
		client - instance of paramiko.SSHClient
		"""
		client.connect(
			ip, username=self.username,
			look_for_keys=False, key_filename=self.key_filename
		)


class SSHAuthKeyDefault(namedtuple('SSHAuthKeyDefault', ('username',))):
	def connect(self, ip, client):
		"""
		ip - server IP address
		client - instance of paramiko.SSHClient
		"""
		client.connect(
			ip, username=self.username,
			look_for_keys=True
		)


def read_ssh_auth(section, required=False):
	from os.path import exists

	if (
		not required and
		'ssh-auth-type' not in section or
		section['ssh-auth-type'].strip().lower() in ('', 'none')
	):
		# No SSH auth is specified. Consider local connections, no SSH connection.
		return None

	if section['ssh-auth-type'] == 'password':
		return SSHAuthPassword(username=section['ssh-username'], password=section['ssh-password'])
	elif section['ssh-auth-type'] == 'key':
		if 'ssh-key' in section:
			if section['ssh-key'] == '':
				return SSHAuthKeyDefault(username=section['ssh-username'])
			else:
				if exists(section['ssh-key']):
					return SSHAuthKeyFilename(username=section['ssh-username'], key_filename=section['ssh-key'])
				else:
					raise Exception(
						messages.SSHKEY_FILE_SPECIFIED_IN_SECTION_S % (
							section.section_name,
						)
					)
		else:
			return SSHAuthKeyDefault(username=section['ssh-username'])
	else:
		if required:
			allowed_values = ['password', 'key']
		else:
			allowed_values = ['password', 'key', 'none']

		error_message = (
			u"Invalid value '%s' of 'ssh-auth-type' parameter in '[%s]' section of configuration file. Allowed values: %s.")
		if not required:
			error_message += messages.IF_NOT_SPECIFIED_NONE_IS_SPECIFIED

		raise MigrationConfigurationFileError(
			error_message % (
				section['ssh-auth-type'],
				section.section_name,
				format_list(allowed_values)
			)
		)



def is_transfer_resource_limits_enabled(config, target_panel):
	if 'transfer-resource-limits' in config.options('GLOBAL'):
		return config.getboolean('GLOBAL', 'transfer-resource-limits')
	else:
		default = target_panel.is_transfer_resource_limits_by_default()
		return default


MultithreadingParams = namedtuple(
	'MultithreadingParams', (
		'status',
		'num_workers',
	)
)


class MultithreadingStatus(object):
	DISABLED = 'disabled'
	DEFAULT = 'default'
	FULL = 'full'


def read_multithreading_params(config):
	global_section = ConfigSection(config, 'GLOBAL', 'multithreading-')
	status = global_section.get('status', MultithreadingStatus.FULL)
	allowed_status_values = {
		MultithreadingStatus.DEFAULT,
		MultithreadingStatus.DISABLED,
		MultithreadingStatus.FULL,
	}
	if status not in allowed_status_values:
		raise MigrationError(
			messages.INVALID_VALUE_S_IS_SPECIFIED_FOR % (
				status, format_list(allowed_status_values)
			)
		)
	return MultithreadingParams(
		status=status,
		num_workers=int(global_section.get('num-workers', '5')),
	)



def read_rsync_additional_args(config):
	global_section = ConfigSection(config, 'GLOBAL')
	default_rsync_additional_args = '' 
	return global_section.get(
		'rsync-additional-args', 
		default_rsync_additional_args
	).split()



class ActionRunnerType(object):
	BY_LAYER = 'layer'
	BY_SUBSCRIPTION = 'subscription'

	@classmethod
	def get_allowed_values(cls):
		return {cls.BY_SUBSCRIPTION, cls.BY_LAYER}

	@classmethod
	def get_default(cls):
		return cls.BY_SUBSCRIPTION


def get_action_runner_type(config):
	global_section = ConfigSection(config, 'GLOBAL')
	runner_type = global_section.get('action-runner', ActionRunnerType.get_default())
	if runner_type not in ActionRunnerType.get_allowed_values():
		raise MigrationError(
			messages.INVALID_VALUE_FOR_ACTIONRUNNER_OPTION_PLEASE % (
				format_list(ActionRunnerType.get_allowed_values()),
			)
		)
	return runner_type



def _read_windows_auth(section, required=True):
	auth = read_auth(section.prefixed('windows-'))

	if required and is_empty(auth.username):
		raise MigrationError(dedent(messages.WINDOWS_USERNAME_IS_NOT_SPECIFIED_FOR) % (
			section.section_name, section.section_name
		))
		
	if required and is_empty(auth.password):
		raise MigrationError(dedent(messages.WINDOWS_PASSWORD_IS_NOT_SPECIFIED_FOR) % (
			section.section_name, section.section_name
		))

	return auth



def _read_poa_connection_settings(section):
	from urlparse import urlparse, urlunparse

	s = read_http_connection_settings(section)
	
	# Put username and password to URL.
	scheme, host, path, params, query, fragment = urlparse(s.url)
	host = '%s:%s@%s' % (s.auth.username, s.auth.password, host)
	url = urlunparse((scheme, host, path, params, query, fragment))

	return HttpConnectionSettings(url=url, auth=s.auth)

