import logging
import re
import pipes

from parallels.hosting_check import DomainIssue, Severity
from parallels.hosting_check import MySQLDatabaseIssueType as IT
from parallels.hosting_check import MySQLClientCLI
from parallels.hosting_check.utils.text_utils import format_list
from parallels.hosting_check import NonZeroExitCodeException

from parallels.hosting_check.messages import MSG

logger = logging.getLogger(__name__)

class MySQLDatabaseChecker(object):
	def check(self, domains_to_check):
		"""
		Arguments:
		- domains_to_check - list of DomainMySQLDatabases
		"""
		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 - DomainMySQLDatabases
		"""
		issues = []

		def add_issue(severity, category, problem):
			issues.append(
				DomainIssue(
					domain_name=domain_to_check.domain_name, 
					severity=severity, 
					category=category, 
					problem=problem
				)
			)

		for database in domain_to_check.databases:
			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
				continue 

			try:
				source_tables = None
				try:
					source_tables = _list_mysql_tables(
						database.name, database.source_access
					)
				except Exception, e:
					logger.debug(u"Exception:", exc_info=e)
					add_issue(
						severity=Severity.WARNING,
						category=IT.FAILED_TO_FETCH_TABLES_FROM_SOURCE,
						problem=MSG(
							IT.FAILED_TO_FETCH_TABLES_FROM_SOURCE,
							host=database.source_access.host, 
							database=database.name,
						)
					)
					
				if source_tables is not None:
					target_tables = _list_mysql_tables(
						database.name, database.target_access
					)
					if not (target_tables >= source_tables):
						add_issue(
							severity=Severity.ERROR, 
							category=IT.DIFFERENT_TABLES_SET,
							problem=MSG(
								IT.DIFFERENT_TABLES_SET,
								database=database.name,
								tables_list=format_list(
									source_tables - target_tables
								)
							)
						)

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

					if user.password_type != 'plain':
						add_issue(
							severity=Severity.WARNING, 
							category=IT.ENCRYPTED_PASSWORD,
							problem=MSG(
								IT.ENCRYPTED_PASSWORD,
								host=database.target_access.host, 
								database=database.name,
								user=user.login,
							)
						)
						continue

					if not _check_db_access(
						database.name, database.target_access, user
					):
						add_issue(
							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,
							)
						)
			except MySQLClientConnectionError, e:
				logger.debug(u"Exception:", exc_info=e)
				add_issue(
					severity=Severity.ERROR, 
					category=IT.CONNECTION_ERROR,
					problem=MSG(
						IT.CONNECTION_ERROR,
						host=database.target_access.host, 
						database=database.name,
					)
				)
			except MySQLClientDatabaseDoesNotExist, e:
				logger.debug(u"Exception:", exc_info=e)
				add_issue(
					severity=Severity.ERROR, 
					category=IT.DATABASE_DOES_NOT_EXIST,
					problem=MSG(
						IT.DATABASE_DOES_NOT_EXIST,
						database=database.name,
						host=database.target_access.host
					)
				)
			except MySQLClientError, e:
				logger.debug(u"Exception:", exc_info=e)
				add_issue(
					severity=Severity.ERROR, 
					category=IT.MYSQL_CLIENT_GENERIC_ERROR,
					problem=MSG(
						IT.MYSQL_CLIENT_GENERIC_ERROR,
						database=database.name, 
						host=database.target_access.host, 
						output=e.stdout + e.stderr
					)
				)
			except Exception, e:
				logger.debug(u"Exception:", exc_info=e)
				add_issue(
					severity=Severity.WARNING, 
					category=IT.INTERNAL_ERROR,
					problem=MSG(
						IT.INTERNAL_ERROR,
						reason=str(e),
						database=database.name
					)
				)

		return issues

	@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_on_target(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

class BaseMySQLClientCLI(MySQLClientCLI):
	"""Base class to execute MySQL client binary with specified arguments 

	Arguments:
	- runner - instance of ServerRunnerBase, underlying transport to 
	  a server with MySQL client binary
	- mysql_bin - path to MySQL client binary (absolute or relative to PATH)
	"""
	def __init__(self, runner, mysql_bin='mysql'):
		self.runner = runner
		self.mysql_bin = mysql_bin

	def connect(self):
		"""Connect to server with the MySQL client binary
		
		Calls connect to underlying transport.
		"""
		self.runner.connect()

	def disconnect(self):
		"""Disconnect from server with the MySQL client binary
		
		Calls closing connection to underlying transport.
		"""
		self.runner.disconnect()

class UnixMySQLClientCLI(BaseMySQLClientCLI):
	"""Execute MySQL client binary with specified arguments on Unix server""" 
	def sh(self, cmd_str, args):
		return self.runner.sh(
			pipes.quote(self.mysql_bin) + " " + cmd_str, args
		)

class WindowsMySQLClientCLI(BaseMySQLClientCLI):
	"""Execute MySQL client binary with specified arguments on Windows server
	""" 
	def sh(self, cmd_str, args):
		return self.runner.sh(
			'cmd /C ""%s" %s"' % (self.mysql_bin, cmd_str,), args
		)

def _list_mysql_tables(database_name, mysql_database_access):
	try:
		stdout = _execute_query(
			database_name, mysql_database_access, 
			mysql_database_access.admin_user,
			'show tables'
		)
		return set(stdout.split())
	except NonZeroExitCodeException, e:
		_check_mysql_error(e.stdout, e.stderr)

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

def _execute_query(database_name, database_access, user, query):
	"""Execute SQL query on database with MySQL client binary

	Returns:
		stdout of executed command
	Raises:
		NonZeroExitCodeException if mysql client binary returned
		non zero exit code
	"""
	database_access.mysql_client_cli.connect()
	try:
		return database_access.mysql_client_cli.sh(
			"--silent --skip-column-names "
			"-h {host} -P {port} -u {user} -p{password} {database} "
			"-e {query}", 
			dict(
				user=user.login, password=user.password, 
				host=database_access.host, port=database_access.port,
				database=database_name, 
				query=query
			)
		)
	finally:
		database_access.mysql_client_cli.disconnect()

def _check_mysql_error(stdout, stderr):
	for s in (stderr, stdout):
		m = re.match('^\s*ERROR (\d+)', s)
		if m is not None:
			# connection error codes, refer to MySQL guides for more
			# information
			# (https://dev.mysql.com/doc/refman/5.5/en/error-messages-client.html)
			if m.group(1) in ('2002', '2003', '2005', '2006', '2013'): 
				raise MySQLClientConnectionError(stdout, stderr)
			# unknown database error, refer to MySQL guides for more
			# information
			# (https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html)
			if m.group(1) == '1049':
				raise MySQLClientDatabaseDoesNotExist(stdout, stderr)

	raise MySQLClientError(stdout, stderr)

class MySQLClientError(Exception):
	def __init__(self, stdout, stderr):
		self.stdout = stdout
		self.stderr = stderr

class MySQLClientConnectionError(MySQLClientError):
	pass

class MySQLClientDatabaseDoesNotExist(MySQLClientError):
	pass

