import argparse
import logging
import os.path
import sys
import traceback
from typing import List, Tuple, Optional, Dict

from wai.logging import set_logging_level, LOGGING_LEVELS, init_logging, LOGGING_WARNING

from kasperl.api import Session
from seppl import enumerate_plugins, is_help_requested, split_args, args_to_objects, Plugin, check_compatibility, \
    save_args
from seppl.io import Reader, BatchFilter, MultiFilter, Writer, execute
from seppl.placeholders import load_user_defined_placeholders, expand_placeholders
from ._exec import load_pipeline, PIPELINE_FORMAT_FILE
from ._help import CommandlineParameter, params_to_short, param_to_help, params_to_parser

DEFAULT_UPDATE_INTERVAL = 1000

PARAM_LOAD_PIPELINE = "--load_pipeline"
PARAM_DUMP_PIPELINE = "--dump_pipeline"


def _default_params() -> List[CommandlineParameter]:
    """
    Returns the default options to use.

    :return: the list of option definitions
    :rtype: list
    """
    return [
        CommandlineParameter(short_opt="-h", long_opt="--help", help="Show basic help message and exit.", action="store_true", is_help=True),
        CommandlineParameter(long_opt="--help-all", help="Show basic help message plus help on all plugins and exit.", action="store_true", is_help=True),
        CommandlineParameter(long_opt="--help-plugin", metavar="NAME", help="Show help message for plugin NAME and exit.", is_help=True),
        CommandlineParameter(short_opt="-u", long_opt="--update_interval", metavar="INTERVAL", help="Outputs the progress every INTERVAL records (default: %d)." % DEFAULT_UPDATE_INTERVAL, type=int, default=DEFAULT_UPDATE_INTERVAL),
        CommandlineParameter(short_opt="-l", long_opt="--logging_level", choices=LOGGING_LEVELS, help="The logging level to use (default: WARN).", default=LOGGING_WARNING),
        CommandlineParameter(short_opt="-b", long_opt="--force_batch", help="Processes the data in batches.", action="store_true"),
        CommandlineParameter(long_opt="--placeholders", metavar="FILE", help="The file with custom placeholders to load (format: key=value)."),
        CommandlineParameter(long_opt=PARAM_LOAD_PIPELINE, metavar="FILE", help="The file to load the pipeline command from."),
        CommandlineParameter(long_opt=PARAM_DUMP_PIPELINE, metavar="FILE", help="The file to dump the pipeline command in."),
    ]


def print_conversion_usage(prog: str, description: str,
                           readers: Dict[str, Plugin], filters: Dict[str, Plugin], writers: Dict[str, Plugin],
                           aliases: List[str] = None, plugin_details: bool = False, generate_plugin_usage=None,
                           additional_params: Optional[List[CommandlineParameter]] = None):
    """
    Prints the program usage to stdout.
    Ensure global options are in sync with parser in parse_args method below.

    :param prog: the conversion executable
    :type prog: str
    :param description: the description of the executable
    :type description: str
    :param readers: the available readers
    :type readers: dict
    :param filters: the available filters
    :type filters: dict
    :param writers: the available writers
    :type writers: dict
    :param aliases: the list of aliases in the registry
    :type aliases: list
    :param plugin_details: whether to output the plugin details as well
    :type plugin_details: bool
    :param generate_plugin_usage: the method for generating the plugin usage
    :param additional_params: the additional parameters for the parser
    :type additional_params: list
    """
    params = _default_params()
    if additional_params is not None:
        params.extend(additional_params)
    print(params_to_short(prog, params))
    print()
    print(description)
    print()
    print("readers (%d):\n" % len(readers) + enumerate_plugins(readers.keys(), aliases=aliases, prefix="   "))
    print("filters (%d):\n" % len(filters) + enumerate_plugins(filters.keys(), aliases=aliases, prefix="   "))
    print("writers (%d):\n" % len(writers) + enumerate_plugins(writers.keys(), aliases=aliases, prefix="   "))
    print()
    print("options:")
    for param in params:
        print(param_to_help(param))
    print()
    if plugin_details and (generate_plugin_usage is not None):
        all_plugins = dict()
        all_plugins.update(readers)
        all_plugins.update(filters)
        all_plugins.update(writers)
        for plugin in sorted(all_plugins.keys()):
            generate_plugin_usage(plugin)


def parse_conversion_args(args: List[str], prog: str, description: str,
                          readers: Dict[str, Plugin], filters: Dict[str, Plugin], writers: Dict[str, Plugin],
                          aliases: List[str] = None, require_reader: bool = True, require_writer: bool = True,
                          generate_plugin_usage=None, exit_on_help: bool = True,
                          additional_params: Optional[List[CommandlineParameter]] = None) -> Optional[Tuple[Optional[Reader], Optional[BatchFilter], Optional[Writer], Session]]:
    """
    Parses the arguments for the conversion tool.

    :param args: the arguments to parse
    :type args: list
    :param prog: the conversion executable
    :type prog: str
    :param description: the description of the executable
    :type description: str
    :param readers: the available readers
    :type readers: dict
    :param filters: the available filters
    :type filters: dict
    :param writers: the available writers
    :type writers: dict
    :param aliases: the list of aliases in the registry
    :type aliases: list
    :param require_reader: whether a reader is required
    :type require_reader: bool
    :param require_writer: whether a writer is required
    :type require_writer: bool
    :param generate_plugin_usage: the method for generating the plugin usage
    :param exit_on_help: whether to perform a sys.exit(0) when help gets requested or just return None
    :type exit_on_help: bool
    :param additional_params: the additional parameters for the parser
    :type additional_params: list
    :return: tuple of (reader, filter, writer, session), the filter can be None
    :rtype: tuple
    """
    logger = logging.getLogger(prog)

    # prepare plugins
    partial = False
    all_plugins = dict()
    all_plugins.update(readers)
    all_plugins.update(filters)
    all_plugins.update(writers)
    handlers = list(all_plugins.keys())

    # help requested?
    help_requested, plugin_details, plugin_name = is_help_requested(args, handlers=handlers, partial=partial)
    if help_requested:
        if (plugin_name is not None) and (generate_plugin_usage is not None):
            generate_plugin_usage(plugin_name)
        else:
            print_conversion_usage(prog, description, readers, filters, writers,
                                   aliases=aliases, generate_plugin_usage=generate_plugin_usage,
                                   plugin_details=plugin_details, additional_params=additional_params)
        if exit_on_help:
            sys.exit(0)
        else:
            return None

    # load_pipeline from file?
    if PARAM_LOAD_PIPELINE in args:
        idx = args.index(PARAM_LOAD_PIPELINE)
        pipeline_file = args[idx+1]
        pipeline_args = load_pipeline(pipeline_file, pipeline_format=PIPELINE_FORMAT_FILE,
                                      remove_convert_prog=True, convert_prog=prog, logger=logger)
        # remove "load pipeline" args
        del args[idx]
        del args[idx]
        # append pipeline args from file
        args.extend(pipeline_args)

    # parse arguments
    parsed = split_args(args, handlers, partial=partial)
    plugins = args_to_objects(parsed, all_plugins, allow_global_options=True)
    reader = None
    writer = None
    filters = []
    for plugin in plugins:
        if isinstance(plugin, Reader):
            if reader is None:
                reader = plugin
                continue
            else:
                raise Exception("Only one reader can be defined!")

        if isinstance(plugin, BatchFilter):
            filters.append(plugin)
            continue

        if isinstance(plugin, Writer):
            if writer is None:
                writer = plugin
                continue
            else:
                raise Exception("Only one writer can be defined!")

        raise Exception("Unhandled plugin type: %s" % str(type(plugin)))

    # checks whether valid pipeline
    if (reader is None) and require_reader:
        raise Exception("No reader defined!")
    if (writer is None) and require_writer:
        raise Exception("No writer defined!")
    if len(filters) == 0:
        filter_ = None
    elif len(filters) == 1:
        filter_ = filters[0]
    else:
        filter_ = MultiFilter(filters=filters)

    # check compatibility
    if writer is not None:
        if filter_ is not None:
            check_compatibility([reader, filter_, writer])
        else:
            check_compatibility([reader, writer])

    # global options?
    # see print_usage() method above
    params = _default_params()
    if additional_params is not None:
        params.extend(additional_params)
    parser = argparse.ArgumentParser()
    params_to_parser(parser, params)
    session = Session(options=parser.parse_args(parsed[""] if ("" in parsed) else []),
                      logger=logger)
    set_logging_level(session.logger, session.options.logging_level)
    if session.options.placeholders is not None:
        if not os.path.exists(session.options.placeholders):
            session.logger.error("Placeholder file not found: %s" % session.options.placeholders)
        else:
            session.logger.info("Loading custom placeholders from: %s" % session.options.placeholders)
            load_user_defined_placeholders(session.options.placeholders)
    if session.options.dump_pipeline is not None:
        # remove "--dump_pipeline ..." args
        dump_args = args[:]
        idx = dump_args.index(PARAM_DUMP_PIPELINE)
        del dump_args[idx]
        del dump_args[idx]
        # save arguments
        pipeline_path = expand_placeholders(session.options.dump_pipeline)
        save_args(dump_args, pipeline_path, handlers=handlers, prog=prog, logger=logger)

    return reader, filter_, writer, session


def perform_conversion(env_var: Optional[str], args: List[str], prog: str, description: str,
                       readers: Dict[str, Plugin], filters: Dict[str, Plugin], writers: Dict[str, Plugin],
                       aliases: List[str] = None, require_reader: bool = True, require_writer: bool = True,
                       generate_plugin_usage=None, additional_params: Optional[List[CommandlineParameter]] = None,
                       pre_initialize=None, post_finalize=None):
    """
    Parses the command-line arguments and performs the conversion.

    :param env_var: the environment variable to obtain the logging level from, can be None
    :type env_var: str
    :param args: the commandline arguments, uses sys.argv if not supplied
    :type args: list
    :param args: the arguments to parse
    :type args: list
    :param prog: the conversion executable
    :type prog: str
    :param description: the description of the executable
    :type description: str
    :param readers: the available readers
    :type readers: dict
    :param filters: the available filters
    :type filters: dict
    :param writers: the available writers
    :type writers: dict
    :param aliases: the list of aliases in the registry
    :type aliases: list
    :param require_reader: whether a reader is required
    :type require_reader: bool
    :param require_writer: whether a writer is required
    :type require_writer: bool
    :param generate_plugin_usage: the method for generating the plugin usage
    :param additional_params: the additional parameters for the parser
    :type additional_params: list
    :param pre_initialize: optional method to execute before the plugins get initialized, takes the session object as only parameter
    :param post_finalize: optional method to execute after the plugins have been finalized, takes the session object as only parameter
    """
    init_logging(env_var=env_var)
    _args = sys.argv[1:] if (args is None) else args
    try:
        reader, filter_, writer, session = parse_conversion_args(
            _args, prog, description, readers, filters, writers,
            aliases=aliases, require_reader=require_reader, require_writer=require_writer,
            generate_plugin_usage=generate_plugin_usage, additional_params=additional_params)
        session.logger.info("options: %s" % str(_args))
        execute(reader, filter_, writer, session, pre_initialize=pre_initialize, post_finalize=post_finalize)
    except Exception:
        traceback.print_exc()
        print("options: %s" % str(_args), file=sys.stderr)
        print_conversion_usage(
            prog, description,
            readers, filters, writers,
            generate_plugin_usage=generate_plugin_usage)
        sys.exit(1)
