import os
from os.path import exists
from collections import namedtuple
from textwrap import dedent
import ntpath

from parallels.core import messages
from parallels.core.registry import Registry
from parallels.core.utils.common_constants import SSH_DEFAULT_PORT, WINDOWS_SOURCE_DEFAULT_SESSION_DIR, \
	UNIX_SOURCE_DEFAULT_SESSION_DIR
from parallels.core.utils.config_utils import ConfigSection, read_auth, Auth, ConfigSelectionOption, \
	read_selection_option, global_section
from parallels.core.utils.common import format_list, is_run_on_windows, is_empty, merge_dicts
from . import MigrationError, MigrationConfigurationFileError
from parallels.core.utils.entity import Entity


class MailContent(ConfigSelectionOption):
	NONE = 'none'
	MESSAGES = 'messages'
	FULL = 'full'

	def __init__(self, is_windows):
		self._is_windows = is_windows

	@property
	def name(self):
		return 'copy-mail-content'

	@property
	def allowed_values(self):
		if self._is_windows:
			return [self.NONE, self.FULL, self.MESSAGES]
		else:
			return [self.NONE, self.FULL]

	@property
	def default(self):
		return self.FULL


class MailImapEncryption(ConfigSelectionOption):
	NONE = 'none'
	SSL = 'ssl'
	TLS = 'tls'

	@property
	def name(self):
		return 'imap-encryption'

	@property
	def allowed_values(self):
		return [self.NONE, self.SSL, self.TLS]

	@property
	def default(self):
		return self.NONE


class PhysicalServerConfig(Entity):
	def __init__(self, server_id, ip, session_dir):
		self._server_id = server_id
		self._ip = ip
		self._session_dir = session_dir

	@property
	def id(self):
		"""For compatibility with old code. Consider to remove as it intersects with Python's 'id'

		Switch to server_id property instead"""
		return self.server_id

	@property
	def server_id(self):
		return self._server_id

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

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

	def is_windows(self):
		raise NotImplementedError()


class PhysicalWindowsServerConfig(PhysicalServerConfig):
	def __init__(self, server_id, ip, session_dir, windows_auth, agent_settings):
		super(PhysicalWindowsServerConfig, self).__init__(server_id, ip, session_dir)
		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


class PhysicalUnixServerConfig(PhysicalServerConfig):
	def __init__(self, server_id, ip, session_dir, ssh_auth):
		super(PhysicalUnixServerConfig, self).__init__(server_id, ip, session_dir)
		self._ssh_auth = ssh_auth

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

	@property
	def is_windows(self):
		return False


class ExternalDBConfigBase(Entity):
	def __init__(self, host, db_type):
		self._host = host
		self._db_type = db_type

	@property
	def host(self):
		return self._host

	@property
	def db_type(self):
		return self._db_type


class ExternalDBConfigUnix(ExternalDBConfigBase, PhysicalUnixServerConfig):
	def __init__(self, server_id, host, db_type, ip, session_dir, ssh_auth):
		ExternalDBConfigBase.__init__(self, host, db_type)
		PhysicalUnixServerConfig.__init__(self, server_id, ip, session_dir, ssh_auth)


class ExternalDBConfigWindows(ExternalDBConfigBase, PhysicalWindowsServerConfig):
	def __init__(self, server_id, host, db_type, ip, session_dir, windows_auth, agent_settings):
		ExternalDBConfigBase.__init__(self, host, db_type)
		PhysicalWindowsServerConfig.__init__(self, server_id, ip, session_dir, windows_auth, agent_settings)


class CopyMailContentSettings(Entity):
	def __init__(
		self, mode, mail_imap_encryption, custom_mail_copy_content_command,
		mailbox_name_separator, mailbox_name_prefix,
		psamailbackup_provider
	):
		self._mode = mode
		self._mail_imap_encryption = mail_imap_encryption
		self._custom_mail_copy_content_command = custom_mail_copy_content_command
		self._mailbox_name_separator = mailbox_name_separator
		self._mailbox_name_prefix = mailbox_name_prefix
		self._psamailbackup_provider = psamailbackup_provider

	@property
	def mode(self):
		"""Mail migration mode - value of MailContent"""
		return self._mode

	@property
	def mail_imap_encryption(self):
		"""IMAP encryption settings - value of MailImapEncryption"""
		return self._mail_imap_encryption

	@property
	def custom_mail_copy_content_command(self):
		"""Custom command as a string for Unix mail migrations - see common.mail for details"""
		return self._custom_mail_copy_content_command

	@property
	def mailbox_name_separator(self):
		"""Mailbox name separator for imapsync

		See parallels.core.content.mail.imapsync.CopyMailImapsync for details
		"""
		return self._mailbox_name_separator

	@property
	def mailbox_name_prefix(self):
		"""Mailbox name prefix for imapsync

		See parallels.core.content.mail.imapsync.CopyMailImapsync for details"""
		return self._mailbox_name_prefix

	@property
	def psamailbackup_provider(self):
		"""Mail backup provider to use when running PSAMailBackup on source server

		:rtype: str | None
		"""
		return self._psamailbackup_provider


def read_copy_mail_content_settings(section, is_windows):
	copy_mail_content = read_selection_option(section, MailContent(is_windows))

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

	mailbox_name_prefix = section.get('mailbox-name-prefix', None)
	mailbox_name_separator = section.get('mailbox-name-separator', None)
	psamailbackup_provider = section.get('psamailbackup-provider', 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,
		psamailbackup_provider=psamailbackup_provider
	)


class WindowsAgentSettings(Entity):
	def __init__(self, enabled, use_ssl, port, client_cert, client_key, server_cert, agent_path):
		self._enabled = enabled
		self._use_ssl = use_ssl
		self._port = port
		self._client_cert = client_cert
		self._client_key = client_key
		self._server_cert = server_cert
		self._agent_path = agent_path

	@property
	def enabled(self):
		return self._enabled

	@property
	def use_ssl(self):
		return self._use_ssl

	@property
	def port(self):
		return self._port

	@property
	def client_cert(self):
		return self._client_cert

	@property
	def client_key(self):
		return self._client_key

	@property
	def server_cert(self):
		return self._server_cert

	@property
	def agent_path(self):
		return self._agent_path


def read_windows_agent_settings(config, section_name, session_dir):
	"""
	:rtype parallels.core.migrator_config.WindowsAgentSettings:
	"""
	section = ConfigSection(config, section_name)
	return WindowsAgentSettings(
		enabled=section.getboolean(
			'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),
		agent_path=section.get('window-agent-path', ntpath.join(session_dir, 'panel-transfer-agent'))
	)


def read_physical_server_settings(config, section_name):
	section = ConfigSection(config, section_name)
	ip = section['ip']
	os_type = section['os']

	if os_type == 'windows':
		session_dir = section.get('session-dir', WINDOWS_SOURCE_DEFAULT_SESSION_DIR)
		windows_auth = read_windows_auth(section)
		windows_agent_settings = read_windows_agent_settings(config, section_name, session_dir)
		return PhysicalWindowsServerConfig(
			server_id=section_name, ip=ip, session_dir=session_dir,
			windows_auth=windows_auth, agent_settings=windows_agent_settings
		)
	else:
		session_dir = section.get('session-dir', UNIX_SOURCE_DEFAULT_SESSION_DIR)
		ssh_auth = read_ssh_auth(section)
		return PhysicalUnixServerConfig(
			server_id=section_name, ip=ip, session_dir=session_dir,
			ssh_auth=ssh_auth
		)


class MailSettingsConfig(Entity):
	def __init__(self, mail_settings):
		self._mail_settings = mail_settings

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


class SourceUnixConfig(PhysicalUnixServerConfig, MailSettingsConfig):
	def __init__(self, server_id, ip, session_dir, ssh_auth, mail_settings):
		PhysicalUnixServerConfig.__init__(self, server_id, ip, session_dir, ssh_auth)
		MailSettingsConfig.__init__(self, mail_settings)


class SourceWindowsConfig(PhysicalWindowsServerConfig, MailSettingsConfig):
	def __init__(self, server_id, ip, session_dir, windows_auth, agent_settings, mail_settings):
		PhysicalWindowsServerConfig.__init__(self, server_id, ip, session_dir, windows_auth, agent_settings)
		MailSettingsConfig.__init__(self, mail_settings)


def read_mail_settings_config(config, section_name):
	section = ConfigSection(config, section_name)
	return MailSettingsConfig(read_copy_mail_content_settings(section, False))


def read_source_server_settings(config, section_name):
	"""Read generic source server settings"""
	mail_settings_config = read_mail_settings_config(config, section_name)
	physical_server_config = read_physical_server_settings(config, section_name)
	source_class = SourceWindowsConfig if physical_server_config.is_windows else SourceUnixConfig
	return source_class(**merge_dicts(physical_server_config.as_dictionary(), mail_settings_config.as_dictionary()))


def read_external_db_settings(config, section_name):
	"""
	:type section_name: str
	:rtype: parallels.core.migrator_config.ExternalDBConfigBase
	"""
	section = ConfigSection(config, section_name)
	host = section['host']
	ip = section['ip']
	os_type = section['os']
	db_type = section['type']

	if os_type == 'windows':
		session_dir = section.get('session-dir', WINDOWS_SOURCE_DEFAULT_SESSION_DIR)
		windows_auth = read_windows_auth(section)
		windows_agent_settings = read_windows_agent_settings(config, section_name, session_dir)
		return ExternalDBConfigWindows(
			server_id=section_name, db_type=db_type, host=host, ip=ip, session_dir=session_dir,
			windows_auth=windows_auth, agent_settings=windows_agent_settings
		)
	else:
		session_dir = section.get('session-dir', UNIX_SOURCE_DEFAULT_SESSION_DIR)
		ssh_auth = read_ssh_auth(section)
		return ExternalDBConfigUnix(
			server_id=section_name, host=host,  db_type=db_type, ip=ip, session_dir=session_dir,
			ssh_auth=ssh_auth
		)


class SSHAuthBase(Entity):
	def __init__(self, port, username):
		self._port = port
		self._username = username

	@property
	def port(self):
		return self._port

	@property
	def username(self):
		return self._username

	def connect(self, ip, client):
		"""
		:param str ip: server IP address to connect by SSH to
		:param paramiko.SSHClient client: paramiko SSH client object
		"""
		raise NotImplementedError()


class SSHAuthPassword(SSHAuthBase):
	def __init__(self, port, username, password):
		super(SSHAuthPassword, self).__init__(port, username)
		self._password = password

	@property
	def password(self):
		return self._password

	def connect(self, ip, client):
		"""
		:param str ip: server IP address to connect by SSH to
		:param paramiko.SSHClient client: paramiko SSH client object
		"""
		client.connect(
			ip, port=self.port,
			username=self.username, password=self.password,
			look_for_keys=False
		)


class SSHAuthKeyFilename(SSHAuthBase):
	def __init__(self, port, username, key_filename):
		super(SSHAuthKeyFilename, self).__init__(port, username)
		self._key_filename = key_filename

	@property
	def key_filename(self):
		return self._key_filename

	def connect(self, ip, client):
		"""
		:param str ip: server IP address to connect by SSH to
		:param paramiko.SSHClient client: paramiko SSH client object
		"""
		client.connect(
			ip, port=self.port, username=self.username,
			look_for_keys=False, key_filename=self.key_filename
		)


class SSHAuthKeyDefault(SSHAuthBase):
	def connect(self, ip, client):
		"""
		:param str ip: server IP address to connect by SSH to
		:param paramiko.SSHClient client: paramiko SSH client object
		"""
		client.connect(
			ip, port=self.port, username=self.username,
			look_for_keys=True
		)


def read_ssh_auth(section, required=False):
	if (
		not required and
		('ssh-auth-type' not in section or section['ssh-auth-type'].strip().lower() in ('', 'none')) and
		not ('ssh-password' in section and 'ssh-auth-type' not in section)
	):
		# No SSH auth is specified. Consider local connections, no SSH connection.
		return None
	port = int(section.get('ssh-port', SSH_DEFAULT_PORT))
	if section.get('ssh-auth-type') == 'password' or ('ssh-password' in section and 'ssh-auth-type' not in section):
		return SSHAuthPassword(
			port=port, username=section.get('ssh-username', 'root'), password=section['ssh-password']
		)
	elif section['ssh-auth-type'] == 'key':
		if 'ssh-key' in section:
			if section['ssh-key'] == '':
				return SSHAuthKeyDefault(port=port, username=section.get('ssh-username', 'root'))
			else:
				if exists(section['ssh-key']):
					return SSHAuthKeyFilename(
						port=port, username=section.get('ssh-username', 'root'), key_filename=section['ssh-key']
					)
				else:
					raise Exception(
						messages.SSHKEY_FILE_SPECIFIED_IN_SECTION_S % (
							section.section_name,
						)
					)
		else:
			return SSHAuthKeyDefault(port=port, username=section.get('ssh-username', 'root'))
	else:
		if required:
			allowed_values = ['password', 'key']
		else:
			allowed_values = ['password', 'key', 'none']

		error_message = messages.INVALID_SSH_AUTH_VALUE
		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(ConfigSelectionOption):
	DISABLED = 'disabled'
	DEFAULT = 'default'
	FULL = 'full'

	@property
	def name(self):
		return 'multithreading-status'

	@property
	def allowed_values(self):
		return [self.DEFAULT, self.DISABLED, self.FULL]

	@property
	def default(self):
		return self.FULL


def read_multithreading_params(config):
	status = read_selection_option(global_section(config), MultithreadingStatus())
	return MultithreadingParams(
		status=status,
		num_workers=int(global_section(config).get('multithreading-num-workers', '5')),
	)


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


def get_local_session_dir(config):
	session_dir_suffix = global_section(config).get('session-dir', 'migration-session')
	return os.path.join(Registry.get_instance().get_var_dir(), 'sessions', session_dir_suffix)


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

	@property
	def name(self):
		return 'action-runner'

	@property
	def allowed_values(self):
		return [self.BY_SUBSCRIPTION, self.BY_LAYER]

	@property
	def default(self):
		return self.BY_SUBSCRIPTION


def read_action_runner_type(config):
	return read_selection_option(global_section(config), ActionRunnerType())


class MSSQLCopyMethod(ConfigSelectionOption):
	"""Way to copy MSSQL databases"""

	# Automatically select available copy method. If we have access to both source and target servers - use
	# native backups, otherwise use SMO text dump that does not require direct access
	AUTO = 'auto'
	# Copy databases with Plesk dbbackup.exe utility, '--copy' command, which uses text dump created with SMO
	TEXT = 'text'
	# Copy databases with MSSQL native backups - T-SQL "BACKUP" and "RESTORE" procedures
	NATIVE = 'native'

	@property
	def name(self):
		return 'copy-mssql-content'

	@property
	def allowed_values(self):
		return [self.AUTO, self.TEXT, self.NATIVE]

	@property
	def default(self):
		return self.AUTO


def read_mssql_copy_method(config):
	return read_selection_option(global_section(config), MSSQLCopyMethod())


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

	if required and is_empty(auth.username):
		auth = Auth(username='Administrator', password=auth.password)

	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