import logging
import pipes

from parallels.hosting_check.utils.text_utils import format_string
from parallels.hosting_check.utils.text_utils import format_list

from parallels.hosting_check import DomainIssue
from parallels.hosting_check import Severity
from parallels.hosting_check import PostgreSQLDatabaseIssueType as IT
from parallels.hosting_check import NonZeroExitCodeException

from parallels.hosting_check.messages import MSG

logger = logging.getLogger(__name__)

class PostgreSQLDatabaseChecker(object):
	def check(self, domains_to_check):
		"""
		Arguments:
		- domains_to_check - list of DomainPostgreSQLDatabases
		"""
		issues = []
		for domain_to_check in domains_to_check:
			issues += self._check_single_domain(domain_to_check)
		return issues


	def _check_single_domain(self, domain_to_check):
		"""
		Arguments:
		- domain_to_check - DomainPostgreSQLDatabases
		"""
		issues = []

		target_panel_databases = None
		if domain_to_check.target_panel_databases is not None:
			target_panel_databases = dict()
			for database in domain_to_check.target_panel_databases:
				target_panel_databases[database.name] = database.users

		for database in domain_to_check.databases:
			try:
				self._check_single_database(domain_to_check, database, issues)
			except KeyboardInterrupt:
				# for compatibility with python 2.4
				raise
			except Exception, e:
				logger.debug(u"Exception:", exc_info=e)
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.WARNING, 
						category=IT.INTERNAL_ERROR,
						problem=MSG(
							IT.INTERNAL_ERROR,
							database=database.name,
							reason=str(e)
						)
					)
				)

		return issues

	def _check_single_database(self, domain_to_check, database, issues):
		logger.debug("Check PostgreSQL database '%s'", database.name)
		if not self._database_exists_panel(
			domain_to_check, database, issues
		):
			# skip all the other checks - no sense to check 
			# if it even was not created in the panel
			return

		if not self._check_login_to_target_db(
			domain_to_check, database, issues
		):
			# skip all the other checks - no sense to check 
			# if we got error when trying to login into target database
			return

		self._check_database_tables(domain_to_check, database, issues)
		self._check_database_users(domain_to_check, database, issues)

	def _check_database_tables(self, domain_to_check, database, issues):
		source_tables = None
		try:
			source_tables = _list_pgsql_tables(
				database.name, database.source_access
			)
		except KeyboardInterrupt:
			# for compatibility with python 2.4
			raise
		except Exception, e:
			issues.append(
				DomainIssue(
					domain_name=domain_to_check.domain_name,
					severity=Severity.ERROR, 
					category=IT.FAILED_TO_FETCH_TABLES_FROM_SOURCE,
					problem=MSG(
						IT.FAILED_TO_FETCH_TABLES_FROM_SOURCE,
						server=database.source_access.host,
						database=database.name,
						reason=str(e),
					) 
				)
			)

		target_tables = None
		if source_tables is not None:
			try:
				target_tables = _list_pgsql_tables(
					database.name, database.target_access
				)
			except KeyboardInterrupt:
				# for compatibility with python 2.4
				raise
			except Exception, e:
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.ERROR, 
						category=\
							IT.FAILED_TO_FETCH_TABLES_FROM_TARGET,
						problem=MSG(
							IT.FAILED_TO_FETCH_TABLES_FROM_TARGET,
							server=database.source_access.host,
							database=database.name,
							reason=str(e),
						) 
					)
				)

		if source_tables is not None and target_tables is not None:
			if not (target_tables >= source_tables):
				difference = source_tables - target_tables
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.ERROR,
						category=IT.DIFFERENT_TABLES_SET,
						problem=MSG(
							IT.DIFFERENT_TABLES_SET,
							database=database.name,
							tables_list=format_list(difference)
						)
					)
				)

	def _check_database_users(self, domain_to_check, database, issues):
		for user in database.users:
			if not self._database_user_exists_panel(
				domain_to_check, database, user, issues
			):
				# proceed to the next checks only if user exist on the
				# target panel
				return

			if not _check_db_access(
				database.name, database.target_access, user
			):
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.ERROR, 
						category=\
							IT.FAILED_TO_EXECUTE_SIMPLE_QUERY_AS_USER,
						problem=MSG(
							IT.FAILED_TO_EXECUTE_SIMPLE_QUERY_AS_USER,
							user=user.login,
							database=database.name
						)
					)
				)

	def _check_login_to_target_db(self, domain_to_check, database, issues):
		try:
			_execute_pgsql_query(
				database.name, database.target_access, 
				database.target_access.admin_user,
				query='select 1'
			)
		except NonZeroExitCodeException, e:
			if 'could not connect to server' in e.stderr:
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.ERROR, 
						category=\
							IT.CONNECTION_ERROR,
						problem=MSG(
							IT.CONNECTION_ERROR,
							database=database.name,
							server=database.target_access.host,
						)
					)
				)
			elif 'database "%s" does not exist' % (database.name) in e.stderr:
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.ERROR, 
						category=\
							IT.DATABASE_DOES_NOT_EXIST,
						problem=MSG(
							IT.DATABASE_DOES_NOT_EXIST,
							database=database.name,
							server=database.target_access.host,
						)
					)
				)
			else:
				issues.append(
					DomainIssue(
						domain_name=domain_to_check.domain_name,
						severity=Severity.ERROR, 
						category=\
							IT.PSQL_CLIENT_GENERIC_ERROR,
						problem=MSG(
							IT.PSQL_CLIENT_GENERIC_ERROR,
							database=database.name,
							server=database.target_access.host,
							stdout=e.stdout,
							stderr=e.stderr
						)
					)
				)

			# in any case that's an error - return False
			return False

		return True

	@staticmethod
	def _database_exists_panel(domain_to_check, database, issues):
		target_panel_databases = None
		if domain_to_check.target_panel_databases is not None:
			target_panel_databases = set([])
			for db in domain_to_check.target_panel_databases:
				target_panel_databases.add(db.name)

		if (
			target_panel_databases is not None 
			and 
			database.name not in target_panel_databases
		):
			issues.append(
				DomainIssue(
					domain_name=domain_to_check.domain_name,
					severity=Severity.ERROR, 
					category=IT.DATABASE_DOES_NOT_EXIST_IN_PANEL,
					problem=MSG(
						IT.DATABASE_DOES_NOT_EXIST_IN_PANEL,
						database=database.name,
					)
				)
			)
			return False
		else:
			return True

	@staticmethod
	def _database_user_exists_panel(domain_to_check, database, user, issues):
		if domain_to_check.target_panel_databases is not None:
			target_panel_databases = dict()
			for db in domain_to_check.target_panel_databases:
				target_panel_databases[db.name] = db.users
			target_panel_users = target_panel_databases[database.name]
		else:
			target_panel_users = None

		if (
			target_panel_users is not None 
			and 
			user.login not in target_panel_users
		):
			issues.append(
				DomainIssue(
					domain_name=domain_to_check.domain_name,
					severity=Severity.ERROR, 
					category=IT.DATABASE_USER_DOES_NOT_EXIST_IN_PANEL,
					problem=MSG(
						IT.DATABASE_USER_DOES_NOT_EXIST_IN_PANEL,
						user=user.login, database=database.name
					)
				)
			)
			return False
		else:
			return True

def _list_pgsql_tables(database_name, database_access):
	query = (
		u"SELECT tablename FROM pg_catalog.pg_tables "
		u"WHERE schemaname NOT IN ('pg_catalog', 'information_schema')"
	)
	output = _execute_pgsql_query(
		database_name, database_access, database_access.admin_user, query
	)
	return set(output.strip().split('\n'))

def _check_db_access(database_name, database_access, user):
	try:
		stdout = _execute_pgsql_query(
			database_name, database_access, user,
			'select 1'
		)
		return stdout.strip() == '1'
	except NonZeroExitCodeException:
		# simply return false if psql client binary returned 
		# non-zero exit code
		return False

def _execute_pgsql_query(database_name, database_access, user, query):
	database_access.runner.connect()
	try:
		conn_options = {
			'PGUSER': user.login,
			'PGPASSWORD': user.password,
			'PGDATABASE': database_name
		}
		if database_access.host != 'localhost':
			conn_options['PGHOST'] = database_access.host
			conn_options['PGPORT'] = database_access.port

		conn_options_str = ' '.join([
			pipes.quote('%s=%s' % (k, v,)) for k, v in sorted(
				conn_options.items(),
				key=lambda e: e[0]
			)
		])

		psql_command = format_string(
			'{conn_options_str} psql -A -t -q -c "{query}"',
			dict(
				conn_options_str=conn_options_str,
				query=query
			)
		)

		return database_access.runner.sh(psql_command)
	finally:
		database_access.runner.disconnect()
