"""Common functions for command line interface of:
- migration between different panel instances
- move subscriptions between nodes within the same panel instance
"""
import argparse
import logging
import os.path
import sys
import textwrap
import time
try:
    import configparser
except ImportError:
    import ConfigParser as configparser

from parallels.core.thirdparties import colorama
from parallels.core import MigrationError, MigrationConfigurationFileError, MigrationNoContextError
from parallels.core import get_logger, MigratorInterruptException
from parallels.core import messages, APIError
from parallels.core.cli.command import CommandTypes, Command
from parallels.core.logging.configuration import LoggingConfiguration
from parallels.core.migration_state import MigrationState
from parallels.core.registry import Registry
from parallels.core.utils.common import open_unicode_file, is_empty, safe_string_repr
from plesk_migrator.compatibility.utils import stringio

logger = get_logger(__name__)


def run(args_parser, create_migration_func, args):
    # determine base directory and store it into registry
    registry = Registry.get_instance()
    log = registry.set_logging_configuration(LoggingConfiguration())

    _set_up_encoding()
    try:
        options = args_parser.parse_args(args)
        registry.set_command_name(options.command_name)
        registry.set_execution_date(time.time())

        colorama.init()
        log.configure(options)

        migration_state = MigrationState(os.path.join(registry.get_var_dir(), 'state'))
        migration_state.load()
        registry.set_migration_state(migration_state)

        config = _read_config(options.MIGRATION_CONFIGURATION_FILE) if options.is_configuration_required else None
        migration = create_migration_func(config, options)

        if options.is_legacy:
            options.method(migration, options)
        else:
            options.method(migration.action_runner)

        return 0
    except APIError as e:
        logger.debug(u'Exception:', exc_info=e)
        logger.error(e.how_to_reproduce)
        return 1
    except MigrationConfigurationFileError as e:
        logger.exception()
        logger.error(messages.S_PLEASE_FIX_MIGRATION_TOOL_CONFIGURATION, e, options.MIGRATION_CONFIGURATION_FILE)
    except MigrationNoContextError as e:
        logger.error(u"%s", e)
        return 1
    except (KeyboardInterrupt, MigratorInterruptException):
        logger.exception()
        logger.info(messages.MIGRATION_STOPPED_BY_USER_REQUEST)
    except MigrationError as e:
        context = getattr(e, 'context', '')
        logger.debug(u"Context: %s", context, exc_info=e)
        errmsg = safe_string_repr(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 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 configparser.NoOptionError as e:
        logger.exception()
        logger.error(
            messages.EXCEPTION_FAILED_TO_READ_CONFIG_ABSENT_OPTION, e.option, e.section,
            options.MIGRATION_CONFIGURATION_FILE
        )
        return 1
    except configparser.NoSectionError as e:
        logger.exception()
        logger.error(messages.FAILED_READ_CONFIGURATION_FILE_THERE_IS, e.section, options.MIGRATION_CONFIGURATION_FILE)
        return 1
    except Exception as e:
        logger.exception()
        context = getattr(e, 'context', '')
        context_msg = _get_context_msg(context)
        logger.error(
            messages.INTERNAL_MIGRATOR_ERROR_S_MIGRATION_IS % (e,) +
            context_msg
        )
        return 1
    finally:
        migration_state = registry.get_migration_state()
        if migration_state:
            migration_state.save()
        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(configparser.RawConfigParser):
        def get(self, section, option, **kwargs):
            """
            Retrive option value from given section
            :type section: str
            :type option: str
            :rtype: str
            """
            return configparser.RawConfigParser.get(self, section, option).strip('"')
    config_parser = 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:
            if sys.version_info.major >= 3:
                contents = fp.read()
                config_parser.readfp(stringio(contents, is_unicode=True))
            else:
                contents = fp.read().encode('utf-8')
                config_parser.readfp(stringio(contents))

    except Exception as e:
        logger.exception()
        raise MigrationError(messages.FAILED_TO_READ_CONFIG_FILE % (config_path, safe_string_repr(e)))

    return config_parser


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)
    """
    if sys.version_info.major == 2:  # Skip this for python 3
        import codecs
        sys.stdout = codecs.getwriter('utf-8')(sys.stdout)


class MigratorHelpCommand(Command):
    def __init__(self, migrator_command_name, commands, indent):
        super(MigratorHelpCommand, self).__init__(
            'help', CommandTypes.MACRO,
            u"Show help", 
            lambda _: self.print_help(),
            args=[
                ['--advanced', messages.SHOW_HELP_ADVANCED_COMMANDS, 'store_true'],
                ['--internal', messages.SHOW_HELP_INTERNAL_COMMANDS, 'store_true']
            ],
            is_configuration_required=False,
            is_enable_profiling_by_default=False,
        )

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

    def print_help(self):
        print(messages.AVAILABLE_COMMANDS)
        options = Registry.get_instance().get_context().options

        def print_command(command_for_print):
            print(self.indent + command_for_print.name)
            print(textwrap.fill(
                command_for_print.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))

        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),
            is_enable_profiling_by_default=False,
            is_legacy=True,
        )

    @staticmethod
    def _shell(migrator, options):
        import code
        banner = messages.AVAILABLE_VARIABLES_MIGRATOR_OPTIONS
        registry = Registry.get_instance()
        registry.get_logging_configuration().configure(
            registry.get_context().options, use_separate_log_by_default=True
        )
        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,
            is_enable_profiling_by_default=False,
        )

    @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):
    """
    :type migrator_command_name: str | unicode 
    :type description: str | unicode 
    :type commands: list[parallels.core.cli.command.Command] 
    :type indent: str | unicode 
    :rtype: argparse.ArgumentParser 
    """
    usage = messages.MIGRATOR_HELP_MESSAGE % (
        # list of commands
        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
        ),
        # config samples path
        os.path.join(Registry.get_instance().get_base_dir(), 'conf', 'samples'),
        # default location of configuration file,
        os.path.join(Registry.get_instance().get_var_dir(), 'conf', 'config.ini')
    )

    args_parser = argparse.ArgumentParser(
        prog=migrator_command_name, 
        description=description, 
        usage=usage, 
        add_help=False
    )
    args_parser.add_argument('--async', help=messages.OPTION_ASYNC_HELP)
    args_parser.add_argument('--quiet', help=messages.OPTION_QUIET_HELP)
    # Internal options
    args_parser.add_argument('--type-checker', help=argparse.SUPPRESS)
    args_parser.add_argument('--logging-config', help=argparse.SUPPRESS)

    subparsers = args_parser.add_subparsers()

    for command in commands:
        usage = u"%s %s %sARGS" % (
            migrator_command_name, command.name,
            u"[MIGRATION_CONFIGURATION_FILE] " if command.is_configuration_required else u""
        )
        cmd = subparsers.add_parser(command.name, help=command.help, parents=command.parents, usage=usage)
        cmd.set_defaults(
            command_name=command.name,
            method=command.method,
            is_configuration_required=command.is_configuration_required,
            is_legacy=command.is_legacy,
            is_skip_progress_reporting=command.is_skip_progress_reporting,
            is_allow_parallel_execution=command.is_allow_parallel_execution,
            is_allow_stop=command.is_allow_stop,
            is_skip_logging=command.is_skip_logging,
            is_enable_profiling_by_default=command.is_enable_profiling_by_default,
            command_operation=command.command_operation
        )
        for (name, help, action) in command.args:
            cmd.add_argument(name, help=help, action=action)
        if command.is_configuration_required:
            cmd.add_argument(
                'MIGRATION_CONFIGURATION_FILE',
                help=messages.CONFIG_FILE_OPTION.format(
                    samples_path=os.path.join(Registry.get_instance().get_base_dir(), 'conf', 'samples'),
                    default_path=os.path.join(Registry.get_instance().get_var_dir(), 'conf', 'config.ini')
                ),
                default='config.ini',
                nargs='?'
            )

    return args_parser
