import logging
import base64

from xml.etree import ElementTree as et

from parallels.ppa import messages
from parallels.core.messages import LOG_EXCEPTION
from parallels.core.utils.common import safe_string_repr, default, xml
from parallels.core.utils.windows_utils import check_windows_national_symbols_command

logger = logging.getLogger(__name__)


class HostNotFoundException(Exception):
	pass


def get_host_id_by_ip(poa_api, ip):
	host_id = poa_api.findHost(ip_address=ip)
	if host_id is None:
		raise HostNotFoundException(messages.HOST_ID_FOR_IP_S_NOT % ip)

	return host_id


class HclPleskdCtlNonZeroExitCodeError(Exception):
	"""Exception that raised in case of non zero exit code from ssh runner"""
	def __init__(self, stdout='', stderr=''):
		self.stdout = stdout
		self.stderr = stderr


class HclDocument(object):
	"""
	Class to represent POA HCL document as ElemetTree tree.
	Pass old_hcl=False for new HCL syntax (POA 6.1 and later)
	"""
	def __init__(self, old_hcl=True):
		self._xml_doc = et.ElementTree(xml.elem('HCL'))
		self._old_hcl = old_hcl

	def declare_vars(self, hcl_vars):
		"""
		:type vars: dict
		:rtype: None
		"""
		declare_elem = self._get_declare_element()
		for name, value in hcl_vars.iteritems():
			declare_elem.append(self._declare_var(name, value))

	def exec_command(self, command, outvar=None, errvar=None, retvar=None, env=None):
		"""
		Execute simple command

		:type command: basestring
		:type outvar: basestring | None
		:type errvar: basestring | None
		:type retvar: basestring | None
		:type env: dict | None
		:rtype: None
		"""
		env_vars = default(env, {})
		exec_elem = xml.elem('EXEC', self._declare_env_vars(env_vars), dict(command=command))

		self._set_elem_attr(exec_elem, 'outvar', outvar)
		self._set_elem_attr(exec_elem, 'errvar', errvar)
		self._set_elem_attr(exec_elem, 'retvar', retvar)

		self._get_perform_element().append(exec_elem)

	def execb64_command(self, command, args=None, stdin=None, outvar=None, errvar=None, retvar=None, env=None):
		"""
		Execute command and pass some content to its stdin (Linux only)

		:type command: basestring
		:type args: list | None
		:type stdin: basestring | None
		:type outvar: basestring | None
		:type errvar: basestring | None
		:type retvar: basestring | None
		:type env: dict | None
		:rtype: None
		"""
		children = []
		if args:
			children.extend(self._declare_args(command, args))
		if env:
			children.extend(self._declare_env_vars(env))

		exec_elem = xml.elem('EXECB64', children, dict(command=command, user='root', group='root'))
		self._set_elem_attr(exec_elem, 'outvar', outvar)
		self._set_elem_attr(exec_elem, 'errvar', errvar)
		self._set_elem_attr(exec_elem, 'retvar', retvar)

		if stdin:
			exec_elem.text = base64.encodestring(stdin)

		self._get_perform_element().append(exec_elem)

	def createfile(self, filename, contents):
		"""
		Create file on disk

		:type filename: basestring
		:type contents: basestring
		:rtype: None
		"""
		createfile_elem = xml.text_elem(
			'CREATEFILEB64', base64.b64encode(contents), dict(path=filename, owner='', group='', overwrite='yes')
		)

		self._get_perform_element().append(createfile_elem)

	def readfile(self, filename, contents_var):
		"""
		Read file from disk and put its content to specified variable

		:type filename: basestring
		:type contents_var: basestring
		:rtype: None
		"""
		self._get_perform_element().append(xml.elem('READB64', None, dict(path=filename, variable=contents_var)))

	def tostring(self):
		"""
		Get HCL document as pretty printed string

		:rtype: basestring
		"""

		doc_head = '''<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<!DOCTYPE HCL PUBLIC "HCL" "HCL 1.0">
'''
		return xml.prettify_xml('%s%s' % (doc_head, xml.create_xml_stream(self._xml_doc, False).read()))

	@staticmethod
	def _set_elem_attr(elem, attr_name, attr_value):
		if attr_value is None:
			return

		elem.set(attr_name, attr_value)

	def _declare_var(self, name, value):
		var_elem = xml.elem('VAR', None, dict(name=name, transient='no'))
		if self._old_hcl:
			var_elem.set('value', value)
		else:
			var_elem.set('attrValue', value)

		return var_elem

	@staticmethod
	def _declare_args(command, args):
		args_list = [xml.elem('ARG', None, dict(value=command))]
		for arg in args:
			args_list.append(xml.elem('ARG', None, dict(value=arg)))

		return args_list

	@staticmethod
	def _declare_env_vars(env):
		env_vars = list()
		for name, value in env.iteritems():
			env_vars.append(xml.elem('ENV_VAR', None, dict(name=name, value=value)))

		return env_vars

	def _get_root_child_element(self, name):
		root = self._xml_doc.getroot()
		if root.find(name) is None:
			root.append(et.Element(name))

		return root.find(name)

	def _get_perform_element(self):
		return self._get_root_child_element('PERFORM')

	def _get_declare_element(self):
		return self._get_root_child_element('DECLARE')


class Hcl(object):
	"""
	Class to perform some POA HCL commands on POA hosts
	"""
	_poa_root = '/usr/local/pem'
	_poa_hcl_format_change_version = (6, 1)

	def __init__(self, ppa_runner):
		self._ppa_runner = ppa_runner
		self._poa_version = self._get_poa_version()

	def run_hcl(self, host_id, script, output_codepage='utf-8', log_script=None, error_policy='strict'):
		log_script = default(log_script, script)

		logger.debug(messages.RUN_HCL_DETAILS, host_id, safe_string_repr(log_script))
		exit_status, stdout_content, stderr_content = self._ppa_runner.sh_unchecked(
			u'cat | %(poa_root)s/bin/pleskd_ctl -f %(poa_root)s/etc/pleskd.props processHCL /dev/stdin %(host_id)s' % {
				'poa_root': self._poa_root, 'host_id': host_id
			},
			stdin_content=script,
			output_codepage=output_codepage,
			error_policy=error_policy
		)
		if exit_status != 0:
			raise HclPleskdCtlNonZeroExitCodeError(stdout_content, stderr_content)

		return stdout_content, stderr_content

	def process_hcl(self, host_id, script, output_codepage='utf-8', log_script=None, error_policy='strict'):
		"""Returns variables dictionary as returned by processHCL"""
		# Stderr contains pleskd debug messages, they are already logged by ssh_utils.run
		# Stdout contains variable values in format "name\x00value\x00name2\x00value2\0xx"
		stdout, _ = self.run_hcl(
			host_id, script, output_codepage=output_codepage, log_script=log_script, error_policy=error_policy
		)

		parts = stdout.split('\x00')[:-1] # Split will return empty string after last '\x00', trim it.
		variables = dict(zip(parts[::2], parts[1::2]))

		return variables

	def get_windows_hcl_script(self, command, env=None):
		hcl = self._get_hcl_document()
		hcl.declare_vars(dict(stdout='', stderr='', exit_code=''))
		hcl.exec_command(command, outvar='stdout', errvar='stderr', retvar='exit_code', env=env)

		return hcl.tostring()

	def windows_exec_unchecked(self, host_id, command, codepage, error_policy='strict', env=None):
		check_windows_national_symbols_command(command)
		script = self.get_windows_hcl_script(command, env)

		variables = self.process_hcl(host_id, script, output_codepage=codepage, error_policy=error_policy)
		exit_code, stdout, stderr = int(variables['exit_code']), variables['stdout'], variables['stderr']

		return exit_code, stdout, stderr

	def upload_file(self, host_id, file_name, file_contents):
		"""Upload file to Windows host"""
		hcl = self._get_hcl_document()
		hcl.createfile(file_name, file_contents)

		log_script = None
		script = hcl.tostring()
		if len(script) > 1024 * 1024: # avoid logging large files
			logger.debug(messages.DUMPING_FULL_HCL_IN_LOG_IS)

			temp_hcl = self._get_hcl_document()
			temp_hcl.createfile(file_name, '')
			log_script = temp_hcl.tostring()
			log_script = log_script.replace('</CREATEFILE', '{CONTENTS OF FILE ENCODED IN BASE64}</CREATEFILE')

		self.process_hcl(host_id, script, log_script=log_script)

	def get_file_contents(self, host_id, file_name):
		out_var = 'contents_base64'

		hcl = self._get_hcl_document()
		hcl.declare_vars({out_var: ''})
		hcl.readfile(file_name, out_var)

		result = self.process_hcl(host_id, hcl.tostring())
		return base64.decodestring(result[out_var])

	def get_unix_hcl_script(self, command, args, stdin_content=None, env=None):
		if args is None:
			args = []

		hcl = self._get_hcl_document()
		hcl.declare_vars(dict(stdout='', stderr='', exit_code=''))
		hcl.execb64_command(command, args, stdin_content, outvar='stdout', errvar='stderr', retvar='exit_code', env=env)

		return hcl.tostring()

	def unix_exec_unchecked(self, host_id, command, args, stdin_content=None, env=None):
		script = self.get_unix_hcl_script(command, args, stdin_content, env)
		variables = self.process_hcl(host_id, script)
		exit_code, stdout, stderr = int(variables['exit_code']), variables['stdout'], variables['stderr']

		return exit_code, stdout, stderr

	def _get_poa_version(self):
		logger.debug(messages.GET_PPA_PLATFORM_VERSION)

		stdout_content = ''
		try:
			stdout_content = self._ppa_runner.sh(
				"psql -Uplesk -h`hostname` -qAt -c 'SELECT version FROM version_history ORDER BY install_date DESC'",
				env=dict(LANG='C')
			)

			version_full = stdout_content.splitlines()[0].strip().split('.')
			if len(version_full) < 2:
				poa_version = (int(version_full[0]), 0)
			else:
				poa_version = (int(version_full[0]), int(version_full[1]))
		except Exception:
			logger.debug(messages.RAW_PPA_PLATFORM_VERSION, stdout_content)
			logger.debug(LOG_EXCEPTION, exc_info=True)
			raise Exception(messages.FAILED_GET_PPA_PLATFORM_VERSION)

		# W/A for PPA 11.6
		# Every PPA/POA/OA contain POA platform version in DB but not PPA 11.6
		if poa_version == (11, 6):
			poa_version = (6, 0)

		return poa_version

	def _get_hcl_document(self):
		return HclDocument(self._poa_version < self._poa_hcl_format_change_version)
