# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 et:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the datalad package for the
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""This is the main() CLI entryproint"""

# It should start-up and run as fast as possible for a responsive CLI.

# Imports are done inline and as late as possible to avoid paying for
# an unconditional commulative overhead that is only actually needed
# in some special cases.

__docformat__ = 'restructuredtext'

import logging

lgr = logging.getLogger('datalad.cli')

lgr.log(5, "Importing cli.main")

import os
import sys

import datalad

from .parser import setup_parser

# TODO cross-check with unconditional imports in .parser
# special case imports
#   from .helpers import _fix_datalad_ri
#   import platform
#   from .helpers import _parse_overrides_from_cmdline
#   from datalad.utils import chpwd
#   from .utils import setup_exceptionhook
#   from datalad.support.exceptions import ...

# unconditional imports, no meaningful functionality without them
#   from .parser import setup_parser


def _on_msys_tainted_paths():
    """This duplicates datalad.utils.on_msys_tainted_paths

    But it does it while minimizing runtime penalties on all irrelevant
    systems.
    """
    if os.environ.get('MSYSTEM', '')[:4] not in ('MSYS', 'MING'):
        return False
    if 'MSYS_NO_PATHCONV' in os.environ:
        return False
    import platform
    if platform.system().lower() != 'windows':
        return False
    return True


def main(args=sys.argv):
    """Main CLI entrypoint"""
    lgr.log(5, "Starting main(%r)", args)
    # record that we came in via the cmdline
    datalad.__api = 'cmdline'
    completing = "_ARGCOMPLETE" in os.environ
    if completing and 'COMP_LINE' in os.environ:
        import shlex

        # TODO support posix=False too?
        args = shlex.split(os.environ['COMP_LINE']) or args

    if _on_msys_tainted_paths():
        # Possibly present DataLadRIs were stripped of a leading /
        from .helpers import _fix_datalad_ri
        args = [_fix_datalad_ri(s) for s in args]

    from datalad.support.entrypoints import load_extensions

    # load extensions requested by configuration
    # analog to what coreapi is doing for a Python session
    # importantly, load them prior to parser construction, such
    # that CLI tuning is also within reach for extensions
    load_extensions()

    # PYTHON_ARGCOMPLETE_OK
    # TODO possibly construct a dedicated parser just for autocompletion
    # rather than lobotomizing the normal one
    parser = setup_parser(args, completing=completing)
    try:
        import argcomplete
        argcomplete.autocomplete(parser)
    except ImportError:
        pass

    # parse cmd args
    lgr.debug("Parsing known args among %r", args)
    cmdlineargs, unparsed_args = parser.parse_known_args(args[1:])
    # did the parser tell us what command to run?
    has_func = hasattr(cmdlineargs, 'func') and cmdlineargs.func is not None
    if unparsed_args:
        if has_func:
            lgr.error('unknown argument%s: %s',
                's' if len(unparsed_args) > 1 else '',
                unparsed_args if len(unparsed_args) > 1 else unparsed_args[0],
            )
            cmdlineargs.subparser.print_usage()
            sys.exit(1)
        else:
            # store all unparsed arguments
            cmdlineargs.datalad_unparsed_args = unparsed_args

    # pull config overrides from cmdline args and put in effect
    if cmdlineargs.cfg_overrides is not None:
        from .helpers import _parse_overrides_from_cmdline
        datalad.cfg.overrides.update(
            _parse_overrides_from_cmdline(cmdlineargs)
        )
        # enable overrides
        datalad.cfg.reload(force=True)
        # try loading extensions again, in case the configuration
        # added new ones to consider
        load_extensions()

    if 'datalad.runtime.librarymode' in datalad.cfg:
        datalad.enable_librarymode()

    if cmdlineargs.change_path is not None:
        from datalad.utils import chpwd
        for path in cmdlineargs.change_path:
            chpwd(path)

    # check argparse could determine what commands needs to be executed
    if not has_func:
        # just let argparser spit out its error, since there is smth wrong
        parser.parse_args(args)
        # if that one didn't puke -- we should
        parser.print_usage()
        lgr.error("Please specify the command")
        # matches exit code for InsufficientArgumentsError
        sys.exit(2)

    _run(cmdlineargs)


def _run(namespace):
    """Execute a CLI operation

    Depending on CLI debugging options the CLI operation is executed
    in a debug harness or an exception handler.

    Parameters
    ----------
    namespace: Namespace
      Object returned by `ArgumentParser.parse_args()` with fully
      populated and validated CLI command and arguments.

    Raises
    ------
    SystemExit
      When the CLI completed without error (exit 0).
    """
    # execute the command, either with a debugger catching
    # a crash, or with a simplistic exception handler.
    # note that result rendering is happening in the
    # execution handler, when the command-generator is unwound
    ret = _run_with_debugger(namespace) \
        if namespace.common_debug or namespace.common_idebug \
        else _run_with_exception_handler(namespace)

    # all good, not strictly needed, but makes internal testing easier
    sys.exit(0)


def _run_with_debugger(cmdlineargs):
    """Execute the command and drop into debugger if it crashes"""
    from .utils import setup_exceptionhook

    # so we could see/stop clearly at the point of failure
    setup_exceptionhook(ipython=cmdlineargs.common_idebug)
    return cmdlineargs.func(cmdlineargs)


def _run_with_exception_handler(cmdlineargs):
    """Execute the command and perform some reporting
    normalization if it crashes, but otherwise just let it go"""
    # otherwise - guard and only log the summary. Postmortem is not
    # as convenient if being caught in this ultimate except
    try:
        return cmdlineargs.func(cmdlineargs)
    # catch BaseException for KeyboardInterrupt
    except BaseException as exc:
        from datalad.support.exceptions import (
            CapturedException,
            CommandError,
            IncompleteResultsError,
            InsufficientArgumentsError,
        )
        ce = CapturedException(exc)
        # we crashed, it has got to be non-zero for starters
        exit_code = 1
        if isinstance(exc, InsufficientArgumentsError):
            # if the func reports inappropriate usage, give help output
            lgr.error('%s (%s)', ce, exc.__class__.__name__)
            cmdlineargs.subparser.print_usage(sys.stderr)
            exit_code = 2
        elif isinstance(exc, IncompleteResultsError):
            # in general we do not want to see the error again, but
            # present in debug output
            lgr.debug('could not perform all requested actions: %s', ce)
        elif isinstance(exc, CommandError):
            exit_code = _communicate_commanderror(exc) or exit_code
        elif isinstance(exc, KeyboardInterrupt):
            from datalad.ui import ui
            ui.error("\nInterrupted by user while doing magic: %s" % ce)
            exit_code = 3
        else:
            # some unforeseen problem
            lgr.error('%s', ce.format_with_cause())
        sys.exit(exit_code)


def _communicate_commanderror(exc):
    """Behave as if the command ran directly"""
    exc_msg = exc.to_str(include_output=False)
    if exc_msg:
        msg = exc_msg.encode() if isinstance(exc_msg, str) else exc_msg
        os.write(2, msg + b"\n")
    # push any captured output to the respective streams
    for out, stream in ((exc.stdout, 1), (exc.stderr, 2)):
        if out:
            os.write(stream,
                     out.encode() if isinstance(out, str) else out)
    # pass on exit code
    return exc.code


lgr.log(5, "Done importing cli.main")
