"""Common functions for command line interface of:
- migration between different panel instances
- move subscriptions between nodes within the same panel instance
"""

from parallels.core import messages, APIError

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

from parallels.core import MigrationError, MigrationConfigurationFileError, MigrationNoContextError
from parallels.core.registry import Registry
from parallels.core.utils.common import open_unicode_file, is_empty
from parallels.core.utils.log import Log

logger = logging.getLogger('parallels')


def run(base_dir, var_dir, execution_path, args_parser, create_migrator_func, args):
	# determine base panel migrator directory and store it into registry
	registry = Registry.get_instance()
	registry.set_base_dir(base_dir)
	registry.set_var_dir(var_dir)
	registry.set_execution_path(execution_path)
	log = registry.set_log(Log())

	_set_up_encoding()
	try:
		options = args_parser.parse_args(args)

		log.configure(options)

		if options.is_migrator_required:
			config_path = options.CONFIG_FILE
			migrator = create_migrator_func(_read_config(config_path))
			if hasattr(migrator, 'set_options'):
				migrator.set_options(options)
			if not options.skip_profiling and hasattr(migrator, 'configure_profiler'):
				migrator.configure_profiler()
			if options.type_checker and hasattr(migrator, 'configure_type_checker'):
				migrator.configure_type_checker()
			if options.is_legacy:
				options.method(migrator, options)
			else:
				options.method(migrator.action_runner)
		else:
			options.method(options)

		return 0
	except APIError as e:
		logger.debug(u'Exception:', exc_info=e)
		logger.error(e.how_to_reproduce)
		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(messages.LOG_EXCEPTION, exc_info=True)
		logger.info(messages.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 = _get_context_msg(context)
		logger.debug(messages.LOG_UNICODE_EXCEPTION, exc_info=e)
		logger.debug(messages.ERROR_OCCURRED_WHILE_WORKING_FOLLOWING_STRING, e.object)
		logger.error(
			messages.INTERNAL_MIGRATOR_ERROR_CAUSED_BY_UNICODE % (e,) + context_msg
		)
		return 1
	except NoOptionError as e:
		logger.debug(messages.LOG_EXCEPTION, exc_info=True)
		logger.error(messages.EXCEPTION_FAILED_TO_READ_CONFIG_ABSENT_OPTION, e.option, e.section, config_path)
	except NoSectionError as e:
		logger.debug(messages.LOG_EXCEPTION, exc_info=True)
		logger.error(messages.FAILED_READ_CONFIGURATION_FILE_THERE_IS, e.section, config_path)
	except MigrationConfigurationFileError as e:
		logger.debug(messages.LOG_EXCEPTION, exc_info=True)
		logger.error(messages.S_PLEASE_FIX_MIGRATION_TOOL_CONFIGURATION, e, config_path)
	except Exception as e:
		context = getattr(e, 'context', '')
		context_msg = _get_context_msg(context)
		logger.debug(messages.LOG_EXCEPTION, exc_info=True)
		logger.error(
			messages.INTERNAL_MIGRATOR_ERROR_S_MIGRATION_IS % (e,) +
			context_msg
		)
		return 1
	finally:
		logging.shutdown()


def _get_context_msg(context):
	if not is_empty(context):
		context_msg = u"\n%s" % messages.EXCEPTION_CONTEXT.format(context=context)
	else:
		context_msg = u''
	return context_msg


def _read_config(raw_config_path):
	default_config_path = os.path.join(Registry.get_instance().get_var_dir(), 'conf', raw_config_path)
	config_path = default_config_path if os.path.exists(default_config_path) else raw_config_path

	Registry.get_instance().set_config_path(config_path)

	# TODO: Workaround for Zend_Config_Writer compatibility, which escape values with doublequotes
	class PanelMigratorConfigParser(RawConfigParser):
		def get(self, section, option):
			"""
			Retrive option value from given section
			:type section: str
			:type option: str
			:rtype: str
			"""
			return RawConfigParser.get(self, section, option).strip('"')
	config = PanelMigratorConfigParser()

	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(messages.LOG_EXCEPTION, exc_info=True)
		raise MigrationError(messages.FAILED_TO_READ_CONFIG_FILE % (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)


class CommandTypes(object):
	MACRO = 'macro'
	MICRO = 'micro'
	INTERNAL = 'internal'


class Command(object):
	def __init__(
		self,
		name,
		type,
		help,
		method,
		parents=None,
		args=None,
		is_migrator_required=True,
		is_legacy=False,
		is_skip_reporting=False
	):
		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.is_migrator_required = is_migrator_required
		self.is_legacy = is_legacy
		self.is_skip_reporting = is_skip_reporting


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', messages.SHOW_HELP_ADVANCED_COMMANDS, 'store_true'],
				['--internal', messages.SHOW_HELP_INTERNAL_COMMANDS, 'store_true']
			],
			is_migrator_required=False
		)

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

	def print_help(self, options):
		print messages.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(
				messages.ADVANCED_COMMANDS_ARE_EXECUTED_AS_SEPARATE,
				width=80
			)
			for command in self.commands:
				if command.type == CommandTypes.MICRO:
					print_command(command)

		if options.internal:
			print 
			print textwrap.fill(
				messages.INTERNAL_COMMANDS_ARE_INTENDED_USE_BY,
				width=80
			)
			for command in self.commands:
				if command.type == CommandTypes.INTERNAL:
					print_command(command)

		if not options.advanced and not options.internal:
			print messages.HOW_TO_SEE_MORE_COMMANDS.format(indent="    ", command=self.migrator_command_name)

		print messages.OPTIONAL_ARGUMENTS.format(indent=self.indent)

		sys.exit(1)


class MigratorShellCommand(Command):
	def __init__(self):
		super(MigratorShellCommand, self).__init__(
			'shell', CommandTypes.INTERNAL,
			messages.START_PYTHON_INTERPRETER_IN_MIGRATOR_ENVIRONMENT,
			lambda m, o: self._shell(m, o)
		)
		self.is_legacy = True
	
	@staticmethod
	def _shell(migrator, options):
		import code
		banner = messages.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,
			messages.START_PYTHON_SCRIPT_IN_MIGRATOR_ENVIRONMENT,
			lambda m, o: self._script(m, o),
			[],
			[
				['script', messages.SCRIPT_TO_EXECUTE_OPTION, 'store']
			],
			is_legacy=True
		)

	@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, indent):
	usage = messages.MIGRATOR_HELP_MESSAGE % (
		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('--panel-migrator-config', help=messages.PATH_PANEL_MIGRATOR_CONFIGURATION_FILE_SEE)
	args_parser.add_argument('--async', help=messages.OPTION_ASYNC_HELP)
	args_parser.add_argument('--quiet', help=messages.OPTION_QUIET_HELP)
	args_parser.add_argument('--skip-profiling', help=messages.OPTION_SKIP_PROFILING_HELP)
	args_parser.add_argument('--type-checker', help=messages.OPTION_TYPE_CHECKER_HELP)

	subparsers = args_parser.add_subparsers()

	for command in commands:
		usage = u"%s %s %sARGS" % (migrator_command_name, command.name, u"CONFIG_FILE " if command.is_migrator_required else u"")
		cmd = subparsers.add_parser(command.name, help=command.help, parents=command.parents, usage=usage)
		cmd.set_defaults(
			method=command.method,
			is_migrator_required=command.is_migrator_required,
			is_legacy=command.is_legacy,
			is_skip_reporting=command.is_skip_reporting
		)
		for (name, help, action) in command.args:
			cmd.add_argument(name, help=help, action=action)
		if command.is_migrator_required:
			cmd.add_argument('CONFIG_FILE', help=messages.CONFIG_FILE_OPTION, default='config.ini', nargs='?')

	return args_parser
