import logging
from parallels.core import messages
from parallels.core.utils.common import ip as ip_utils

from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import list_databases, check_connection
from parallels.core.utils.common import cached, contextmanager, is_empty

logger = logging.getLogger(__name__)


class DatabaseServer(object):
	"""Base interface for working with database servers in migrator"""

	def description(self):
		if self.host() == 'localhost':
			return messages.DATABASE_SERVER_DESCRIPTION % (self.type_description, self.ip(), self.port())
		else:
			return messages.DATABASE_SERVER_DESCRIPTION % (self.type_description, self.host(), self.port())

	@property
	def type_description(self):
		db_server_types = {
			DatabaseServerType.MYSQL: 'MySQL',
			DatabaseServerType.POSTGRESQL: 'PostgreSQL',
			DatabaseServerType.MSSQL: 'MSSQL'
		}
		return db_server_types.get(self.type().lower(), self.type())

	@cached
	def ip(self):
		if self.physical_server is not None:
			# if we have physical server - take IP from it
			return self.physical_server.ip()

		if self.host() == 'localhost':
			# 'localhost' means database server, local to panel server
			return self.panel_server.ip()

		# host may contain MS SQL instance name, e.g. "10.50.52.100\MSSQLSERVER2008"
		host = self.host().split('\\')[0]
		if host == '.':
			# the same as 'localhost', which means local to panel server
			return self.panel_server.ip()

		return ip_utils.resolve(host)

	def type(self):
		raise NotImplementedError()

	def host(self):
		raise NotImplementedError()

	def port(self):
		raise NotImplementedError()

	def user(self):
		raise NotImplementedError()

	@cached
	def password(self):
		raise NotImplementedError()

	@contextmanager
	def runner(self):
		"""Alias for utilities_server.runner function"""
		with self.utilities_server.runner() as runner:
			yield runner

	def get_session_file_path(self, filename):
		"""Alias for utilities_server.get_session_file_path function"""
		return self.utilities_server.get_session_file_path(filename)

	def session_dir(self):
		"""Alias for utilities_server.session_dir function"""
		return self.utilities_server.session_dir()

	def is_windows(self):
		"""Alias for utilities_server.is_windows function"""
		return self.utilities_server.is_windows()

	@property
	def panel_server(self):
		"""Get panel server that controls this database server.

		For example, single panel Plesk server may control one MySQL server on the same physical server (on localhost)
		and an external MySQL server located on another physical server. For both MySQL database servers,
		this function will return single object that corresponds to Plesk server.
		"""
		raise NotImplementedError()

	@property
	def physical_server(self):
		"""Get physical server on which database server is located.

		By physical we mean the server, where database files, processes and services are running.
		If we don't have access to the physical server, this function must return None.

		:rtype: parallels.core.connections.physical_server.PhysicalServer | None
		"""
		raise NotImplementedError()

	@property
	def utilities_server(self):
		"""Get server on which migrator should run database utilities to work with that database server

		This major aspect here is that the server must have access to database server by database protocols.
		Also it should have database utilities installed, but that is not guaranteed.
		"""
		# TODO clarify return type
		if self.physical_server is not None:
			# If we have access to physical server, or it is the same as panel server - return it
			return self.physical_server
		else:
			# If we don't have access to physical server, try to execute database utilities on panel server.
			# Usually it provides utilities (mysqldump, pg_dump, mysql, psql, etc), but sometimes - not,
			# and there could be version mistmatch
			return self.panel_server

	def list_databases(self):
		"""List databases on that server.

		Returns list of databases names or None if that function is not supported for that database server type.

		:rtype: set[basestring] | None
		"""
		return list_databases(self)

	def check_connection(self):
		"""Check connection to that database server.

		:raises parallels.core.utils.database_utils.DatabaseServerConnectionException:
		"""
		return check_connection(self)

	@property
	def mysql_bin(self):
		return None

	def __hash__(self):
		return hash((self.type(), self.host(), self.port()))

	def __eq__(self, other):
		return (
			isinstance(self, DatabaseServer) and
			other.type() == self.type() and other.host() == self.host() and other.port() == self.port() and
			other.panel_server == self.panel_server
		)


class ExternalDatabaseServer(DatabaseServer):
	"""External database server that could be accessed directly.

	For such database servers customer should specify access settings in configuration file.
	"""
	def __init__(self, settings, panel_server, port, user, password, physical_server):
		"""
		:type settings: parallels.core.migrator_config.ExternalDBConfigBase
		:type physical_server: parallels.core.connections.physical_server.PhysicalServer
		"""
		self._settings = settings
		self._panel_server = panel_server
		self._port = port
		self._password = password
		self._user = user
		self._physical_server = physical_server

	def type(self):
		return self._settings.db_type

	def host(self):
		return self._settings.host

	def port(self):
		return self._port

	def user(self):
		return self._user

	def password(self):
		if not is_empty(self._password):
			return self._password
		else:
			# Try to detect password
			return self.panel_server.get_database_server_password(self)

	@property
	def panel_server(self):
		return self._panel_server

	@property
	def physical_server(self):
		return self._physical_server


class SourceDatabaseServer(DatabaseServer):
	"""Source database server (either local or external) accessed from panel server"""
	def __init__(self, access_settings, model, panel_server):
		self._access_settings = access_settings
		self._model = model
		self._panel_server = panel_server

	def type(self):
		return self._model.dbtype

	def host(self):
		return self._model.host

	def port(self):
		return self._model.port

	def user(self):
		return self._model.login

	def password(self):
		if not is_empty(self._model.password):
			return self._model.password
		else:
			# Try to detect password
			return self.panel_server.get_database_server_password(self)

	@property
	def physical_server(self):
		if is_local_database_host(self.host(), [self._panel_server.ip()]):
			# This database is located on local panel server
			return self._panel_server
		else:
			# This database is located on external server
			# We don't have physical access to it, so return None
			return None

	@property
	def panel_server(self):
		return self._panel_server


def is_local_database_host(host, ips):
	"""Check if specified database host name is local to server with specified IPs

	:type host: str | unicode
	:type ips: list[str | unicode]
	"""
	return host.split('\\')[0] in {'localhost', '.', '127.0.0.1'} | set(ips)