import os
import sys
from functools import update_wrapper

import click

from django import get_version, VERSION as DJANGO_VERSION
from django.core.management import CommandError


class OptionParseAdapter(object):
    """Django pre-1.10-compatible adapter, deprecated"""

    def parse_args(self, args):
        return (self, None)  # NOCOV


class ArgumentParserDefaults(object):
    def __init__(self, args):
        self._args = args

    def _get_kwargs(self):
        return {
            "args": self._args,
        }


class ArgumentParserAdapter(object):
    def __init__(self):
        self._actions = []
        self._mutually_exclusive_groups = []

    def parse_args(self, args):
        return ArgumentParserDefaults(args)


class DjangoCommandMixin(object):
    use_argparse = False
    option_list = []
    base_stealth_options = []

    @property
    def stealth_options(self):
        return sum(
            ([p.name] + [i.lstrip("-") for i in p.opts] for p in self.params), [],
        )

    def invoke(self, ctx):
        try:
            return super(DjangoCommandMixin, self).invoke(ctx)
        except CommandError as e:
            # Honor the --traceback flag
            if ctx.traceback:  # NOCOV
                raise
            styled_message = click.style(
                "{}: {}".format(e.__class__.__name__, e), fg="red", bold=True
            )
            click.echo(styled_message, err=True)
            ctx.exit(1)

    def run_from_argv(self, argv):
        """
        Called when run from the command line.
        """
        prog_name = "{} {}".format(os.path.basename(argv[0]), argv[1])
        try:
            # We won't get an exception here in standalone_mode=False
            exit_code = self.main(
                args=argv[2:], prog_name=prog_name, standalone_mode=False
            )
            if exit_code:
                sys.exit(exit_code)
        except click.ClickException as e:
            if getattr(e.ctx, "traceback", False):  # NOCOV
                raise
            e.show()
            sys.exit(e.exit_code)

    def create_parser(self, progname, subcommand):
        """
        Called when run through `call_command`.
        """
        if DJANGO_VERSION >= (1, 10):
            return ArgumentParserAdapter()
        else:  # NOCOV
            return OptionParseAdapter()

    def print_help(self, prog_name, subcommand):
        prog_name = "{} {}".format(prog_name, subcommand)
        self.main(["--help"], prog_name=prog_name, standalone_mode=False)

    def map_names(self):
        for param in self.params:
            for opt in param.opts:
                yield opt.lstrip("--").replace("-", "_"), param.name

    def execute(self, *args, **kwargs):
        """
        Called when run through `call_command`. `args` are passed through,
        while `kwargs` is the __dict__ of the return value of
        `self.create_parser('', name)` updated with the kwargs passed to
        `call_command`.
        """
        # Remove internal Django command handling machinery
        kwargs.pop("skip_checks", None)
        parent_ctx = click.get_current_context(silent=True)
        with self.make_context("", list(args), parent=parent_ctx) as ctx:
            # Rename kwargs to to the appropriate destination argument name
            opt_mapping = dict(self.map_names())
            arg_options = {
                opt_mapping.get(key, key): value for key, value in kwargs.items()
            }

            # Update the context with the passed (renamed) kwargs
            ctx.params.update(arg_options)

            # Invoke the command
            self.invoke(ctx)

    def __call__(self, *args, **kwargs):
        """
        When invoked, normal click commands act as entry points for command
        line execution. When using Django, commands get invoked either through
        the `execute_from_command_line` or `call_command` utilities.

        Calling a command directly can thus be just a shortcut for calling its
        `execute` method.
        """
        return self.execute(*args, **kwargs)


class CommandAdapter(DjangoCommandMixin, click.Command):
    pass


class GroupAdapter(DjangoCommandMixin, click.Group):
    pass


def register_on_context(ctx, param, value):
    setattr(ctx, param.name, value)
    return value


def suppress_colors(ctx, param, value):
    # Only set the value if a flag was provided, otherwise we would override
    # the default set by the parent context if one was available.
    if value is not None:
        ctx.color = value
    return value


class BaseRegistrator(object):
    common_options = [
        click.option(
            "-v",
            "--verbosity",
            expose_value=False,
            default="1",
            callback=register_on_context,
            type=click.IntRange(min=0, max=3),
            help=(
                "Verbosity level; 0=minimal output, 1=normal "
                "output, "
                "2=verbose output, 3=very verbose output."
            ),
        ),
        click.option(
            "--settings",
            metavar="SETTINGS",
            expose_value=False,
            help=(
                "The Python path to a settings module, e.g. "
                '"myproject.settings.main". If this is not provided, the '
                "DJANGO_SETTINGS_MODULE environment variable will be used."
            ),
        ),
        click.option(
            "--pythonpath",
            metavar="PYTHONPATH",
            expose_value=False,
            help=(
                "A directory to add to the Python path, e.g. "
                '"/home/djangoprojects/myproject".'
            ),
        ),
        click.option(
            "--traceback/--no-traceback",
            is_flag=True,
            default=False,
            expose_value=False,
            callback=register_on_context,
            help="Raise on CommandError exceptions.",
        ),
        click.option(
            "--color/--no-color",
            default=None,
            expose_value=False,
            callback=suppress_colors,
            help=(
                "Enable or disable output colorization. Default is to "
                "autodetect the best behavior."
            ),
        ),
    ]

    def __init__(self, **kwargs):
        self.kwargs = kwargs
        self.version = self.kwargs.pop("version", get_version())

        context_settings = kwargs.setdefault("context_settings", {})
        context_settings["help_option_names"] = ["-h", "--help"]

    def get_params(self, name):
        def show_help(ctx, param, value):
            if value and not ctx.resilient_parsing:
                click.echo(ctx.get_help(), color=ctx.color)
                ctx.exit()

        return [
            click.version_option(version=self.version, message="%(version)s"),
            click.option(
                "-h",
                "--help",
                is_flag=True,
                is_eager=True,
                expose_value=False,
                callback=show_help,
                help="Show this message and exit.",
            ),
        ] + self.common_options

    def __call__(self, func):
        module = sys.modules[func.__module__]

        # Get the command name as Django expects it
        self.name = func.__module__.rsplit(".", 1)[-1]

        # Build the click command
        decorators = [
            click.command(name=self.name, cls=self.cls, **self.kwargs),
        ] + self.get_params(self.name)

        for decorator in reversed(decorators):
            func = decorator(func)

        # Django expects the command to be callable (it instantiates the class
        # pointed at by the `Command` module-level property)...
        # ...let's make it happy.
        module.Command = lambda: func

        return func


def pass_verbosity(f):
    """
    Marks a callback as wanting to receive the verbosity as a keyword argument.
    """

    def new_func(*args, **kwargs):
        kwargs["verbosity"] = click.get_current_context().verbosity
        return f(*args, **kwargs)

    return update_wrapper(new_func, f)


class CommandRegistrator(BaseRegistrator):
    cls = CommandAdapter


class GroupRegistrator(BaseRegistrator):
    cls = GroupAdapter
