#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Main module for cpac package."""

import argparse
from itertools import chain
import logging
import os
import sys

from docker.errors import DockerException, NotFound

from cpac import __version__
from cpac.backends import Backends
from cpac.helpers import cpac_parse_resources as parse_resources, TODOs

_logger = logging.getLogger(__name__)
_CLARGS: set = {"group", "utils"}
"""commandline arguments to pass into container after `--`"""


class ExtendAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        items = (getattr(namespace, self.dest) or []) + values
        items = [x for n, x in enumerate(items) if x not in items[:n]]
        setattr(namespace, self.dest, items)


def address(str_addy):
    addr, port = str_addy.split(":")
    port = int(port)
    return addr, port


def help_call(argument_list: list) -> bool:
    """Check if the helpstring is being called."""
    return "--help" in argument_list or "-h" in argument_list


def _parser():
    """Generate parser.

    Parameters
    ----------
    None

    Returns
    -------
    parser : argparse.ArgumentParser
    """
    cwd = os.getcwd()

    parser = argparse.ArgumentParser(
        description="cpac: a Python package that simplifies using C-PAC "
        "<http://fcp-indi.github.io> containerized images. \n\n"
        "This commandline interface package is designed to "
        "minimize repetition.\nAs such, nearly all arguments are "
        "optional.\n\nWhen launching a container, this package "
        "will try to bind any paths mentioned in \n • the command"
        "\n • the data configuration\n\nAn example minimal run "
        "command:\n\tcpac run /path/to/data /path/for/outputs"
        "\n\nAn example run command with optional arguments:\n\t"
        "cpac -B /path/to/data/configs:/configs \\\n\t\t"
        "--image fcpindi/c-pac --tag latest \\\n\t\t"
        "run /path/to/data /path/for/outputs \\\n\t\t"
        "--data_config_file /configs/data_config.yml \\\n\t\t"
        "--save_working_dir\n\n"
        'Each command can take "--help" to provide additonal '
        "usage information, e.g.,\n\n\tcpac run --help\n\n"
        "Known issues:\n"
        + "\n".join([f"- {todo}" for todo in TODOs.values()])
        + "\n- https://github.com/FCP-INDI/cpac/issues",
        conflict_handler="resolve",
        formatter_class=argparse.RawTextHelpFormatter,
    )

    parser.add_argument(
        "--version",
        action="version",
        version="cpac (convenience wrapper) version {ver}\nFor C-PAC version, "
        "run `cpac version` with any cpac options (e.g., "
        "`--platform`, `--image`, `--tag`) that you would use "
        "while running".format(ver=__version__),
    )

    parser.add_argument(
        "-o",
        "--container_option",
        dest="container_option",
        action="append",
        help="parameters and flags to pass through to Docker or Singularity\n"
        "\nThis flag can take multiple arguments so cannot "
        "be\nthe final argument before the command argument (i.e.,\nrun "
        "or any other command that does not start with - or --)\n",
        metavar="OPT",
    )

    parser.add_argument(
        "-B",
        "--custom_binding",
        dest="custom_binding",
        action="append",
        help="directories to bind with a different path in\nthe container "
        "than the real path of the directory.\nOne or more pairs in the "
        "format:\n\treal_path:container_path\n(eg, "
        "/home/C-PAC/run5/outputs:/outputs).\nUse absolute paths for "
        "both paths.\n\nThis flag can take multiple arguments so cannot "
        "be\nthe final argument before the command argument (i.e.,\nrun "
        "or any other command that does not start with - or --)\n",
    )

    parser.add_argument(
        "--platform",
        choices=["docker", "singularity"],
        help="If neither platform nor image is specified,\ncpac will try "
        "Docker first, then try\nSingularity if Docker fails.",
    )

    parser.add_argument(
        "--image",
        help="path to Singularity image file OR name of Docker image (eg, "
        '"fcpindi/c-pac").\nWill attempt to pull from Singularity Hub or '
        "Docker Hub if not provided.\nIf image is specified but platform "
        "is not, platform is\nassumed to be Singularity if image is a "
        "path or \nDocker if image is an image name.",
    )

    parser.add_argument(
        "--tag", help='tag of the Docker image to use (eg, "latest" or "nightly").'
    )

    parser.add_argument(
        "--working_dir", help="working directory", default=cwd, metavar="PATH"
    )

    parser.add_argument(
        "-v",
        "--verbose",
        dest="loglevel",
        help="set loglevel to INFO",
        action="store_const",
        const=logging.INFO,
    )

    parser.add_argument(
        "-vv",
        "--very-verbose",
        dest="loglevel",
        help="set loglevel to DEBUG",
        action="store_const",
        const=logging.DEBUG,
    )

    subparsers = parser.add_subparsers(dest="command")

    run_parser = subparsers.add_parser(
        "run",
        add_help=False,
        help='Run C-PAC. See\n"cpac [--platform {docker,singularity}] '
        '[--image IMAGE] [--tag TAG] run --help"\nfor more '
        "information.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    utils_parser = subparsers.add_parser(
        "utils",
        add_help=False,
        help='Run C-PAC commandline utilities. See\n"cpac [--platform '
        "{docker,singularity}] [--image IMAGE] [--tag TAG] utils "
        '--help"\nfor more information.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    version_parser = subparsers.add_parser(
        "version", add_help=True, help="Print the version of C-PAC that cpac is using."
    )

    # run_parser.add_argument('--address', action='store', type=address)

    if not help_call(sys.argv):
        # These positional arguments are required unless we're just getting
        # the helpstring
        run_parser.add_argument("bids_dir")
        run_parser.add_argument("output_dir", default=os.path.join(cwd, "outputs"))
        run_parser.add_argument(
            "level_of_analysis", choices=["participant", "group", "test_config"]
        )
    run_parser.add_argument("--data_config_file", metavar="PATH")
    run_parser.add_argument("extra_args", nargs=argparse.REMAINDER)

    group_parser = subparsers.add_parser(
        "group",
        add_help=False,
        help='Run a group level analysis in C-PAC. See\n"cpac [--platform '
        "{docker,singularity}] [--image IMAGE] [--tag TAG] group "
        '--help"\nfor more information.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    subparsers.add_parser(
        "pull",
        add_help=True,
        help="Upgrade your local C-PAC version to the latest version\n"
        "by pulling from Docker Hub or other repository.\nUse with "
        '"--image" and/or "--tag" to specify an image\nother than '
        'the default "fcpindi/c-pac:latest" to pull.',
        aliases=["upgrade"],
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    enter_parser = subparsers.add_parser(
        "enter",
        add_help=True,
        help="Enter a new C-PAC container via BASH.",
        aliases=["bash", "shell"],
    )

    parse_resources.set_args(
        subparsers.add_parser(
            "parse-resources",
            add_help=True,
            aliases=["parse_resources"],
            help="\n".join(
                [
                    parse_resources.__doc__.split(
                        parse_resources.__file__.split("/", maxsplit=-1)[-1], maxsplit=1
                    )[-1]
                    .strip()
                    .replace(r"`cpac_parse_resources`", '"parse-resources"'),
                    'See "cpac parse-resources --help" for more information.',
                ]
            ),
        )
    )

    crash_parser = subparsers.add_parser(
        "crash",
        add_help=True,
        help="Convert a crash pickle to plain text (C-PAC < 1.8.0).",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    crash_parser.add_argument(
        "crashfile",
        help="path to crashfile, to read a crashfile from C-PAC < 1.8.0.\n"
        "Crashfiles in C-PAC >= 1.8.0 are plain text files.",
    )

    for subparser in [
        crash_parser,
        enter_parser,
        group_parser,
        run_parser,
        utils_parser,
        version_parser,
    ]:
        subparser.register("action", "extend", ExtendAction)

    return parser


def parse_args(args):
    """Parse commandline arguments.

    Parameters
    ----------
    args : list

    Returns
    -------
    parsed : Namespace
    """
    parser = _parser()
    parsed, extras = parser.parse_known_args(args)

    parsed.extra_args = [
        *(parsed.extra_args if hasattr(parsed, "extra_args") else []),
        *extras,
    ]

    return parsed


def setup_logging(loglevel):
    logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s"
    logging.basicConfig(
        level=loglevel, stream=sys.stdout, format=logformat, datefmt="%Y-%m-%d %H:%M:%S"
    )


def main(args):
    """Connect to C-PAC container and perform specified action.

    Parameters
    ----------
    args : Namespace

    Returns
    -------
    None
    """
    if any("--data_config_file" in arg for arg in args.extra_args):
        try:
            args.data_config_file = args.extra_args[
                args.extra_args.index("--data_config_file") + 1
            ]
        except ValueError:
            try:
                args.data_config_file = next(
                    arg.split("=", 1)[1]
                    for arg in args.extra_args
                    if arg.startswith("--data_config_file=")
                )
            except Exception:  # pragma: no cover
                raise ValueError(
                    f"""Something about {[
                        arg for arg in args.extra_args if
                        '--data_config_file' in arg
                    ]} is confusing."""
                )
    else:
        args.data_config_file = (
            args.data_config_file if hasattr(args, "data_config_file") else None
        )

    args.bids_dir = args.bids_dir if hasattr(args, "bids_dir") else "bids_dir"

    setup_logging(args.loglevel)

    arg_vars = vars(args)

    if args.command == "run":
        if any(
            [
                "--help" in arg_vars,
                "-h" in arg_vars,
                "--help" in args.extra_args,
                "-h" in args.extra_args,
            ]
        ):
            pwd = os.getcwd()
            if arg_vars.get("level_of_analysis") is None:
                arg_vars["level_of_analysis"] = "participant"
            for arg in ["output_dir", "bids_dir"]:
                if arg_vars.get(arg) is None:
                    arg_vars[arg] = pwd
        Backends(**arg_vars).run(flags=args.extra_args, **arg_vars)

    elif args.command in ["enter", "version"]:
        Backends(**arg_vars).run(
            run_type=args.command, flags=args.extra_args, **arg_vars
        )

    elif args.command in ["pull", "upgrade"]:
        Backends(**arg_vars).pull(force=True, **arg_vars)

    elif args.command in _CLARGS:
        # utils doesn't have '-h' flag for help
        if args.command == "utils" and "-h" in arg_vars.get("extra_args", []):
            arg_vars["extra_args"] = [
                arg if arg != "-h" else "--help" for arg in arg_vars["extra_args"]
            ]
        Backends(**arg_vars).clarg(args.command, flags=args.extra_args, **arg_vars)

    elif args.command == "crash":
        Backends(**arg_vars).read_crash(flags=args.extra_args, **arg_vars)

    elif args.command == "parse-resources":
        parse_resources.main(args)


def run():
    """
    Try Docker first and fall back on Singularity if Docker fails if --platform is not specified.

    Parameters
    ----------
    None

    Returns
    -------
    None

    Notes
    -----
    Consumes commandline arguments. Run `cpac --help` for usage string.
    """
    args = sys.argv[1:]
    # reorder args
    command = None
    command_index = 0
    parser = _parser()
    if args and (args[0] == "--version" or args[0] == "--help" or args[0] == "-h"):
        parse_args(args)
    # pylint: disable=protected-access
    commands = list(
        next(
            cmd for cmd in parser._get_positional_actions() if cmd.dest == "command"
        ).choices
    )
    options = set(
        chain.from_iterable([o.option_strings for o in parser._get_optional_actions()])
    )
    # keep help option with specific command
    for option in ("-h", "--help"):
        options.discard(option)
    for cmd in commands:
        if command is None and cmd in args:
            command_index = args.index(cmd)
            command = args.pop(command_index)
    if command is None:
        parser.print_help()
        parser.exit()
    reordered_args = []
    option_value_setting = False
    for i, arg in enumerate(args.copy()):
        if i == command_index:
            option_value_setting = False
        if arg in options:
            reordered_args.append(args.pop(args.index(arg)))
            option_value_setting = True
        elif any(arg.startswith(f"{option}=") for option in options):
            reordered_args.append(args.pop(args.index(arg)))
            option_value_setting = True
        elif option_value_setting:
            if arg.startswith("-"):
                option_value_setting = False
            else:
                reordered_args.append(args.pop(args.index(arg)))
    args = [*reordered_args, command, *args]
    # parse args
    parsed = parse_args(args)
    if not parsed.platform and "--platform" not in args:
        if parsed.image and os.path.exists(parsed.image):
            parsed.platform = "singularity"
        else:
            parsed.platform = "docker"
        try:
            main(parsed)
        # fall back on Singularity if Docker not found
        except (DockerException, NotFound):  # pragma: no cover
            parsed.platform = "singularity"
            main(parsed)
    else:
        main(parsed)


if __name__ == "__main__":
    run()
