# CLI common functions for transfer and move subscriptions between nodes

import sys
import logging
import logging.config
import argparse
import os.path
import yaml
import textwrap
from StringIO import StringIO
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError

from parallels.common import MigrationError, MigrationConfigurationFileError, MigrationNoContextError
from parallels.utils import obj
from parallels.utils import open_unicode_file
from parallels.plesk_api.core import PleskError
from parallels.poa_api import PoaApiError

logger = logging.getLogger('parallels')

def run(args_parser, create_migrator_func, args):
	_set_up_encoding()
	try:
		options = args_parser.parse_args(args)

		if os.path.exists(options.log_config):
			with open(options.log_config) as f:
				logging.config.dictConfig(yaml.load(f))
		elif os.path.exists('/etc/panel-migrator/logging.config'):
			with open('/etc/panel-migrator/logging.config') as f:
				logging.config.dictConfig(yaml.load(f))
		logging.captureWarnings(True)

		if options.migrator_required:
			config_path = options.CONFIG_FILE
			migrator = create_migrator_func(_read_config(config_path))
			if hasattr(migrator, 'set_options'):
				migrator.set_options(options)
			options.method(migrator, options)
		else:
			options.method(options)

		return 0
	except PoaApiError as e:
		logger.debug(u'Exception:', exc_info=e)
		logger.error(
			u"An internal error occurred in POA XML-RPC: %s.\n"
			u"If you referring to a technical support representative, please attach a stack trace with the error message from debug.log.\n"
			u"To execute a POA XML-RPC request manually: \n"
			u"1. Open debug.log, copy the POA XML-RPC request from the end of the file, and paste it into a new file (/tmp/xmlrpc-packet for example)\n"
			u"""2. Execute the request with the help of the command "curl https://<management_node_ip>:8440/RPC2 -uadmin:<password> -s -k -d @/tmp/xmlrpc-packet"\n"""
			u"   Replace <management_node_ip> and <password> with the appropriate IP address and password."
			% unicode(e)
		)
	except PleskError as e:
		logger.debug(u'Exception:', exc_info=e)
		logger.error(
			u"Unexpected Plesk API Error: %s.\n"
			u"If you referring to a technical support representative, please attach a stack trace with the error message from debug.log.\n"
			u"To execute a Plesk API request manually: \n"
			u"1. Open debug.log and copy the Plesk API request from the end of the file.\n"
			u"""2. Execute it with the help of the command "curl https://<plesk_api_server_ip>:8443/enterprise/control/agent.php -s -d "<quoted_packet>" -H "HTTP_AUTH_LOGIN: admin" -H "HTTP_AUTH_PASSWD: <password>" -H "Content-Type: text/xml" --insecure"\n"""
			u"   Replace <plesk_api_server_ip> with the required IP address, <quoted_packet> with the Plesk API request from debug.log, and <password> with the appropriate password."
			% unicode(e)
		)
		return 1
	except MigrationError as e:
		context = getattr(e, 'context', '')
		logger.debug(u"Context: %s", context, exc_info=e)
		errmsg = unicode(e)
		if errmsg.endswith('.'):
			errmsg = errmsg[:-1]
		if context != '':
			logger.error(u"%s\nContext: %s.", errmsg, context)
		else:
			logger.error(u"%s.", errmsg)
		return 1
	except KeyboardInterrupt as e:
		logger.debug(u"Exception:", exc_info=e)
		logger.info("Migration stopped by user request")
	except MigrationNoContextError as e:
		logger.error(u"%s", e)
		return 1
	except UnicodeError as e:
		context = getattr(e, 'context', '')
		context_msg = u"\nContext: %s" % (context,) if context != '' else ''
		logger.error(u"Unicode exception:", exc_info=e)
		logger.error(u"Error occured while working with the following string: %r", e.object)
		logger.error(u"Unexpected error: '%s', migration is aborted. See the traceback and the string that caused failure above for more information." % (e,) + context_msg)
		return 1
	except NoOptionError as e:
		logger.debug(u"Exception:", exc_info=e)
		logger.error(u"Failed to read configuration file: no option '%s' in section '[%s]'. Please fix migration tool configuration file ('%s') and run migration tool again." % (
			e.option, e.section, config_path
		))
	except NoSectionError as e:
		logger.debug(u"Exception:", exc_info=e)
		logger.error(u"Failed to read configuration file: there is no section '[%s]'. Please fix migration tool configuration file ('%s') and run migration tool again." % (
			e.section, config_path
		))
	except MigrationConfigurationFileError as e:
		logger.debug(u"Exception:", exc_info=e)
		logger.error(u"%s. Please fix migration tool configuration file ('%s') and run migration tool again." % (
			e, config_path
		))
	except Exception as e:
		context = getattr(e, 'context', '')
		context_msg = u"\nContext: %s" % (context,) if context != '' else ''
		logger.error(u"Exception:", exc_info=e)
		logger.error(u"Unexpected error: '%s', migration is aborted. See the traceback above for more information." % (e,) + context_msg)
		return 1
	finally:
		logging.shutdown()

def _read_config(config_path):
	config = RawConfigParser()

	try:
		# work with encodings so config.ini with UTF-8 BOM
		# is readable on Windows (notepad saves in UTF-8 BOM by default)
		with open_unicode_file(config_path) as fp:
			contents = fp.read().encode('utf-8')

		config.readfp(StringIO(contents))
	except Exception as e:
		logger.debug(u"Exception:", exc_info=True)
		raise MigrationError(u"Failed to read configuration file '%s': %s" % (config_path, str(e)))

	return config

def _set_up_encoding():
	"""Set up encoding to 'UTF-8', so python does not fail with "UnicodeEncodeError: 'ascii' codec can't encode characters in position ..." even in cases:
	1) if we do not use terminal (redirect output to pipe, like 'python migrator config.ini check | less')
	2) if we use some encoding, that do not support all range of symbols we output (for example, 'C' and any cyrillic symbol)
	Also there is another way described at http://stackoverflow.com/questions/1473577/writing-unicode-strings-via-sys-stdout-in-python
	but in case #2 it could make migration stop (while in the current solution we continue migration, but output "bad" symbols)
	"""
	import codecs
	sys.stdout = codecs.getwriter('utf-8')(sys.stdout)

CommandTypes = obj(MACRO = 'macro', MICRO = 'micro', INTERNAL = 'internal')

class Command(object):
	def __init__(self, name, type, help, method, parents=None, args=None, migrator_required=True):
		self.name = name
		self.type = type
		self.help = help
		self.method = method
		if parents is not None:
			self.parents = parents
		else:
			self.parents = []
		if args is not None:
			self.args = args
		else:
			self.args = []
		self.migrator_required = migrator_required

class MigratorHelpCommand(Command):
	def __init__(self, migrator_command_name, commands, indent):
		super(MigratorHelpCommand, self).__init__(
			'help', CommandTypes.MACRO,
			u"Show help", 
			self.print_help,
			args=[
				['--advanced', u'Show help on advanced commands', 'store_true'],
				['--internal', u'Show help on internal commands', 'store_true']
			],
			migrator_required=False
		)

		self.migrator_command_name = migrator_command_name
		self.commands = commands
		self.indent = indent

	def print_help(self, options):
		print u'Available commands:'
		def print_command(command):
			print self.indent + command.name
			print textwrap.fill(
				command.help, width = 80, 
				initial_indent = self.indent * 2, subsequent_indent = self.indent * 2
			)

		for command in self.commands:
			if command.type == CommandTypes.MACRO:
				print_command(command)

		if options.advanced:
			print 
			print textwrap.fill(u'The advanced commands are executed as separate steps of the commands listed in the main section, but can be invoked separately for problem resolution or more granular migration process. Advanced commands:', width=80)
			for command in self.commands:
				if command.type == CommandTypes.MICRO:
					print_command(command)

		if options.internal:
			print 
			print textwrap.fill(u'The internal commands are intended to use by migration tool developers. Internal commands:', width=80)
			for command in self.commands:
				if command.type == CommandTypes.INTERNAL:
					print_command(command)

		if not options.advanced and not options.internal:
			print u"To see more commands run"
			print u"%s%s help --advanced" % (
				self.indent,
				self.migrator_command_name
			)

		print """
	Optional arguments:
	{indent}--log-config LOG_CONFIG_FILE
	{indent}{indent}Path to logging configuration file. 
	{indent}{indent}See http://docs.python.org/library/logging.config.html
	{indent}-h, --help Show help message of a command
	""".format(indent=self.indent)

		sys.exit(1)

class MigratorShellCommand(Command):
	def __init__(self):
		super(MigratorShellCommand, self).__init__(
			'shell', CommandTypes.INTERNAL,
			u"Start Python interpreter in migrator environment",
			lambda m, o: self._shell(m, o)
		)
	
	@staticmethod
	def _shell(migrator, options):
		import code
		banner = u"Available variables: migrator, options."
		code.interact(banner, local=dict(migrator=migrator, options=options))

class MigratorScriptCommand(Command):
	def __init__(self):
		super(MigratorScriptCommand, self).__init__(
			'script', CommandTypes.INTERNAL,
			u"Start Python script in migrator environment",
			lambda m, o: self._script(m, o),
			[],
			[
				['script', u"Script to execute", 'store']
			]
		)

	@staticmethod
	def _script(migrator, options):
		import runpy
		runpy.run_path(options.script, init_globals=dict(migrator=migrator, options=options), run_name='__main__')

def create_arguments_parser(migrator_command_name, description, commands, script_dir, indent):
	usage = u"""%%(prog)s [--log-config LOG_CONFIG_FILE] COMMAND
commands: 
%s
To view detailed help run "%%(prog)s help".
To view arguments of a command COMMAND run "%%(prog)s COMMAND -h".
""" % (
		textwrap.fill(
			', '.join([command.name for command in commands if command.type == CommandTypes.MACRO]),
			width = 80, break_on_hyphens = False, 
			initial_indent = indent, subsequent_indent = indent
		)
	)

	args_parser = argparse.ArgumentParser(
		prog=migrator_command_name, 
		description=description, 
		usage=usage, 
		add_help=False
	)
	args_parser.add_argument('--log-config', default=os.path.join(script_dir, "logging.config"),
		help=u"""
Path to logging configuration file.
See http://docs.python.org/library/logging.config.html#configuration-file-format for configuration file format."""
	)

	subparsers = args_parser.add_subparsers()

	for command in commands:
		usage = u"%s %s %sARGS" % (migrator_command_name, command.name, u"CONFIG_FILE " if command.migrator_required else u"")
		cmd = subparsers.add_parser(command.name, help=command.help, parents=command.parents, usage=usage)
		cmd.set_defaults(method=command.method, migrator_required=command.migrator_required)
		for (name, help, action) in command.args:
			cmd.add_argument(name, help=help, action=action)
		if command.migrator_required:
			cmd.add_argument('CONFIG_FILE', help=u'migrator configuration file')

	return args_parser

