import argparse
import io
import json
from abc import ABC, abstractmethod
from collections import Counter
import logging
from contextlib import redirect_stderr
from pathlib import Path

from amilib.util import Util

"""Base class for specialist sub_parsers .
Also includes ArgParseBuilder
"""

logger = Util.get_logger(__name__)
logger.setLevel(logging.INFO)

OPERATION = "operation"
class AbstractArgs(ABC):

    PIPELINE = "Pipeline"
    DEBUG = "debug"
    INDIR = "indir"
    INPUT = "input"
    INFORMAT = "informat"
    KWARGS = "kwords"
    OPERATION = "operation"
    OUTPUT = "output"
    OUTDIR = "outdir"

    DEBUG_HELP = f"will output debugging information (not fully implemented) \n"

    INPUT_HELP = f"input from:\n" \
                 f"   file/s single, multiple, and glob/wildcard (experimental)\n" \
                 f"   directories NYI\n" \
                 f"   URL/s (must start with 'https:'); provide {OUTDIR} for output \n"

    INDIR_HELP = f"Directory containing input files\n"

    OPERATION_HELP = "operation to be performed (--operation is normally required)"
    OUTPUT_HELP = "output file or similar resource"

    OUTDIR_HELP = "output directory, required for URL input. If not given, autogenerated from file names"

    def get_input_help(self):
        return self.INPUT_HELP

    def get_indir_help(self):
        return self.INDIR_HELP

    def get_output_help(self):
        return self.OUTPUT_HELP

    def get_outdir_help(self):
        return self.OUTDIR_HELP

    def __init__(self):
        self.operation = None
        self.parser = None
        self.parsed_args = None
        self.ref_counter = Counter()
        self.arg_dict = self.create_default_arg_dict()
        self.subparser_arg = "UNKNOWN"

    def create_arg_dict(self, args=None):
        """
        takes args or self.parsed_args as a list of items(tuples) and creates a dictionary (self.arg_dict)
        :param args: list of key-values
        :return: dict() created form this
        """
        logger.debug(f"create_arg_dict args: {args}")
        if args:
            self.parsed_args = args
        if not self.parsed_args:
            return None
        try:
            arg_vars = vars(self.parsed_args)
        except TypeError:
            arg_vars = self.parsed_args
        self.arg_dict = dict()
        for item in arg_vars.items():
            key = item[0]
            if item[1] is None:
                pass
            elif type(item[1]) is list and len(item[1]) == 1:
                self.arg_dict[key] = item[1][0]
            else:
                self.arg_dict[key] = item[1]

        return self.arg_dict

    def _parse_and_process1(self, argv_):
        """
        Takes args from CLI , creates self.arg_dict, and passes to self.process_args()
        :param argv_: name-value pairs usually from CLI

        """
        # logger.debug("running parse_and_process1 in util?")
        # logging.warning(f"********** args for parse_and_process1 {argv_}")
        self.parsed_args = argv_ if self.parser is None else self.parser.parse_args(argv_)
        self.arg_dict = self.create_arg_dict()
        self.process_args()

    def add_arguments(self):
        """
        TODO Needs refactoring
        Called by subclasses
        maybe overridden by abstract add_arguments
        """

        self.parser.add_argument(f"--{self.DEBUG}",
                                 action='store_true',
                                 help=self.DEBUG_HELP)

        self.parser.add_argument(f"--{self.INPUT}", nargs="+",
                                 help=self.INPUT_HELP)

        self.parser.add_argument(f"--{self.INDIR}", nargs="+",
                                 help=self.INDIR_HELP)

        self.parser.add_argument(f"--{self.OUTPUT}",
                                 help=self.OUTPUT_HELP)

        self.parser.add_argument(f"--{self.OUTDIR}",
                                 help=self.OUTDIR_HELP)

        # self.parser.add_argument(f"--{self.OPERATION}", nargs="+",
        #                          help=self.OPERATION_HELP)

        INFORM_HELP = "input format/s; experimental"
        self.parser.add_argument(f"--{self.KWARGS}", nargs="*",
                help="space-separated list of colon_separated keyword-value pairs, "
                     "format kw1:val1 kw2:val2;\nif empty list gives help")


    @abstractmethod
    def process_args(self):
        pass

    @abstractmethod
    def create_default_arg_dict(self):
        pass

    @abstractmethod
    def add_arguments(self):
        # Overrides add_arguments
        pass

    def get_operation(self):
        """
        NOT USED, maybe it should be
        The operation to run (makes this explicit)
        """
        operation = self.arg_dict.get(AbstractArgs.OPERATION)
        return operation

    def get_indir(self):
        indir = self.arg_dict.get(AbstractArgs.INDIR)
        return indir

    # are these used?
    def get_input(self):
        inputx = self.arg_dict.get(AbstractArgs.INPUT)
        return inputx

    def get_outdir(self):
        outdir = self.arg_dict.get(AbstractArgs.OUTDIR)
        return outdir

    def get_output(self):
        output = self.arg_dict.get(AbstractArgs.OUTPUT)
        return output


    """
    These kwargs methods may by used by subclasses
    """
    def parse_kwargs_to_string(self, kwargs, keys=None):

        kwargs_dict = {}
        logger.info(f"args: {kwargs}")
        if not kwargs:
            if keys:
                logger.debug(f"possible keys: {keys}")
        else:
            if type(kwargs) is not list:
                kwargs = [kwargs]
            for arg in kwargs:
                logger.debug(f"pair {arg}")
                argz = arg.split(':')
                key = argz[0].strip()
                value = argz[1].strip()
                kwargs_dict[key] = value
            logger.warning(f"kwargs_dict {kwargs_dict}")
        return kwargs_dict


    def get_kwargs(self):
        """
        used if we ever start using **kwargs,
        currently not used
        """
        kwargs = self.arg_dict.get(AbstractArgs.KWARGS)
        logger.debug(f"kwargs {kwargs}")
        if kwargs is None:
            return None
        if len(kwargs) == 0:
            self.kwargs_help()
        else:
            pass

        return


    def kwargs_help(self):
        """
        only used by **kwargs options
        """
        logger.debug(f"key value pairs separated by ':' ; normally explicitly offered by subclass ")


    @classmethod
    def make_sub_parser(cls, subclass, subparsers):
        """make subparser from subparsers
        requires self.subparser_arg (probably should be argument
        ALSO adds arguments through `self.add_arguments
        :param subclass: name of the subclass`
        :param subparsers: list of subparsers (to append subparser to)
        :return: new subparser"""
        subclass.parser = subparsers.add_parser(subclass.subparser_arg)
        logger.debug(f"subclass_parser for {subclass} is {subclass.parser}")
        subclass.add_arguments()
        return subclass.parser

    @classmethod
    def parse_error(cls, parser, args, split_string="error: "):
        """
        captures stderr from parser.parse(args) , splits at split_string
        and returns cause of error. split_string is probably machine/os-specific
        on my Mac was "_jb_pytest_runner.py:<error message>"
        :param parser: argparser
        :param args: args to parse
        :param split_string: string after which machine-independent error is found
               (default'error:')
        :return:None if no error or the error_string
        The string is of the form
        error: argument --operation: invalid choice: 'search' (choose from ...
        and we start after "error: "
        """
        if parser is None or args is None:
            logger.error("must give parser and args")
            return "bad arguments to parse_error()"
        stderr = io.StringIO()
        with redirect_stderr(stderr):
            try:
                parser.parse_args(args)
            except TypeError as te:
                return f"args: {args} \ngives error: {te}"
            except SystemExit:
                error_output = stderr.getvalue()
                logger.error(f"Captured argparse error:\n<<{error_output}>>")
                if error_output is None or error_output.strip() == '':
                    return None
                splits = error_output.split(split_string)
                if (splits is None or len(splits) == 1):
                    return f"Unparsable: {error_output}"
                return splits[1]
        return None



class ArgParseBuilder:
    """
    Not sure whether this is necessary but there is a test using it
    """
    ARG_LIST = "arg_list"
    DESCRIPTION = "description"

    def __init__(self):
        self.parser = None

    def create_arg_parse(self, arg_dict=None, arg_dict_file=None):
        # arg_dict_file takes precedence
        if arg_dict_file and arg_dict_file.exists():
            with open(arg_dict_file, 'r') as f:
                data = f.read()
                arg_dict = json.loads(data)
                logger.debug(f"arg_dict {arg_dict}")

        if arg_dict is not None:
            desc = f'{arg_dict.get(self.DESCRIPTION)}'
            logger.debug(f"\ndesc: '{desc}'")
            self.parser = argparse.ArgumentParser(description=desc)
            arg_list = arg_dict.get(self.ARG_LIST)
            if arg_list is None:
                raise ValueError(f"must give arg_list to ArgParseBuilder")
            for arg_dict in arg_list:
                if not type(arg_dict) is dict:
                    raise ValueError(f"arg_list_dict {arg_dict} is not a dict")
                args = arg_dict.keys()
                for arg in args:
                    logger.debug(f"\n{arg}:")
                    param_dict = arg_dict.get(arg)
                    self.process_params(param_dict)
                # self.parser.add_argument(f"--{ProjectArgs.PROJECT}", type=str, nargs=1, help="project directory")

    """https://stackoverflow.com/questions/28348117/using-argparse-and-json-together"""

    def process_params(self, param_dict):
        for param, param_val in param_dict.items():
            logger.debug(f"  {param}='{param_val}'")


class AmiArgParseException(Exception):
    """
    to capture error messages from AmiArgparser
    """
    pass


class AmiArgParser(argparse.ArgumentParser):
    """
    subclasses ArgumentParser and overrides error()
    """

    def error(self, message):
        """
        raises self.exit(2, error_message) so can be caught
        """
        raise AmiArgParseException(message)


