import re
from pathlib import Path

from pipenv.project import Project
from pipenv.utils import console, err
from pipenv.utils.display import format_help
from pipenv.utils.internet import is_valid_url
from pipenv.vendor.click import (
    BadArgumentUsage,
    BadParameter,
    Group,
    Option,
    argument,
    make_pass_decorator,
    option,
)
from pipenv.vendor.click import types as click_types
from pipenv.vendor.click_didyoumean import DYMMixin

CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"], "auto_envvar_prefix": "PIPENV"}


class PipenvGroup(DYMMixin, Group):
    """Custom Group class provides formatted main help"""

    def get_help_option(self, ctx):
        """Override for showing formatted main help via --help and -h options"""
        help_options = self.get_help_option_names(ctx)
        if not help_options or not self.add_help_option:
            return

        def show_help(ctx, param, value):
            if value and not ctx.resilient_parsing:
                console.print(format_help(ctx.get_help()))
                ctx.exit()

        return Option(
            help_options,
            is_flag=True,
            is_eager=True,
            expose_value=False,
            callback=show_help,
            help="Show this message and exit.",
        )

    def main(self, *args, **kwargs):
        """
        to specify the windows_expand_args option to avoid exceptions on Windows
        see: https://github.com/pallets/click/issues/1901
        """
        return super().main(*args, **kwargs, windows_expand_args=False)


class State:
    def __init__(self):
        self.index = None
        self.verbose = False
        self.quiet = False
        self.pypi_mirror = None
        self.python = None
        self.site_packages = None
        self.clear = False
        self.system = False
        self.project = Project()
        self.installstate = InstallState()
        self.lockoptions = LockOptions()


class InstallState:
    def __init__(self):
        self.dev = False
        self.pre = False
        self.ignore_pipfile = False
        self.code = False
        self.requirementstxt = None
        self.deploy = False
        self.packages = []
        self.editables = []
        self.extra_pip_args = []
        self.categories = []
        self.skip_lock = False


class LockOptions:
    def __init__(self):
        self.dev_only = False


pass_state = make_pass_decorator(State, ensure=True)


def index_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.index = value
        return value

    return option(
        "-i",
        "--index",
        expose_value=False,
        envvar="PIP_INDEX_URL",
        help="Specify target package index by url or index name from Pipfile.",
        nargs=1,
        callback=callback,
    )(f)


def editable_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.editables.extend(value)
        return value

    return option(
        "-e",
        "--editable",
        expose_value=False,
        multiple=True,
        callback=callback,
        type=click_types.Path(file_okay=False),
        help="An editable Python package URL or path, often to a VCS repository.",
    )(f)


def ignore_pipfile_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.ignore_pipfile = value
        return value

    return option(
        "--ignore-pipfile",
        is_flag=True,
        default=False,
        expose_value=False,
        help="Ignore Pipfile when installing, using the Pipfile.lock.",
        callback=callback,
        type=click_types.BOOL,
        show_envvar=True,
    )(f)


def _dev_option(f, help_text):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.dev = value
        return value

    return option(
        "--dev",
        "-d",
        is_flag=True,
        default=False,
        type=click_types.BOOL,
        help=help_text,
        callback=callback,
        expose_value=False,
        show_envvar=True,
    )(f)


def categories_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            state.installstate.categories += re.split(r", *| ", value)
        return value

    return option(
        "--categories",
        nargs=1,
        required=False,
        callback=callback,
        expose_value=True,
        type=click_types.STRING,
    )(f)


def install_dev_option(f):
    return _dev_option(f, "Install both develop and default packages")


def lock_dev_option(f):
    return _dev_option(f, "Generate both develop and default requirements")


def uninstall_dev_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.dev = value
        if value:
            state.installstate.categories.append("dev-packages")
        return value

    return option(
        "--dev",
        "-d",
        is_flag=True,
        default=False,
        type=click_types.BOOL,
        help="Uninstall packages from dev-packages.",
        callback=callback,
        expose_value=False,
        show_envvar=True,
    )(f)


def pre_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.pre = value
        return value

    return option(
        "--pre",
        is_flag=True,
        default=False,
        help="Allow pre-releases.",
        callback=callback,
        type=click_types.BOOL,
        expose_value=False,
    )(f)


def package_arg(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.packages.extend(value)
        return value

    return argument(
        "packages",
        nargs=-1,
        callback=callback,
        expose_value=True,
        type=click_types.STRING,
    )(f)


def extra_pip_args(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            state.installstate.extra_pip_args += value.split(" ")
        return value

    return option(
        "--extra-pip-args",
        nargs=1,
        required=False,
        callback=callback,
        expose_value=True,
        type=click_types.STRING,
    )(f)


def python_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value is not None:
            state.python = validate_python_path(ctx, param, value)
        return value

    return option(
        "--python",
        default="",
        nargs=1,
        callback=callback,
        help="Specify which version of Python virtualenv should use.",
        expose_value=False,
        allow_from_autoenv=False,
        type=click_types.STRING,
    )(f)


def pypi_mirror_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        value = value or state.project.s.PIPENV_PYPI_MIRROR
        if value is not None:
            state.pypi_mirror = validate_pypi_mirror(ctx, param, value)
        return value

    return option(
        "--pypi-mirror",
        nargs=1,
        callback=callback,
        help="Specify a PyPI mirror.",
        expose_value=False,
    )(f)


def verbose_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            if state.quiet:
                raise BadArgumentUsage(
                    "--verbose and --quiet are mutually exclusive! Please choose one!",
                    ctx=ctx,
                )
            state.verbose = True
            setup_verbosity(ctx, param, 1)

    return option(
        "--verbose",
        "-v",
        is_flag=True,
        expose_value=False,
        callback=callback,
        help="Verbose mode.",
        type=click_types.BOOL,
    )(f)


def quiet_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            if state.verbose:
                raise BadArgumentUsage(
                    "--verbose and --quiet are mutually exclusive! Please choose one!",
                    ctx=ctx,
                )
            state.quiet = True
            setup_verbosity(ctx, param, -1)

    return option(
        "--quiet",
        "-q",
        is_flag=True,
        expose_value=False,
        callback=callback,
        help="Quiet mode.",
        type=click_types.BOOL,
    )(f)


def site_packages_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        validate_bool_or_none(ctx, param, value)
        state.site_packages = value
        return value

    return option(
        "--site-packages/--no-site-packages",
        is_flag=True,
        default=None,
        help="Enable site-packages for the virtualenv.",
        callback=callback,
        expose_value=False,
        show_envvar=True,
    )(f)


def clear_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.clear = value
        return value

    return option(
        "--clear",
        is_flag=True,
        callback=callback,
        type=click_types.BOOL,
        help="Clears caches (pipenv, pip).",
        expose_value=False,
        show_envvar=True,
    )(f)


def system_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value is not None:
            state.system = value
        return value

    return option(
        "--system",
        is_flag=True,
        default=False,
        help="System pip management.",
        callback=callback,
        type=click_types.BOOL,
        expose_value=False,
        show_envvar=True,
    )(f)


def requirementstxt_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            state.installstate.requirementstxt = value
        return value

    return option(
        "--requirements",
        "-r",
        nargs=1,
        default="",
        expose_value=False,
        help="Import a requirements.txt file.",
        callback=callback,
        type=click_types.Path(dir_okay=False),
    )(f)


def dev_only_flag(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        if value:
            state.lockoptions.dev_only = value
        return value

    return option(
        "--dev-only",
        default=False,
        is_flag=True,
        expose_value=False,
        help="Emit development dependencies *only* (overrides --dev)",
        callback=callback,
    )(f)


def deploy_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.deploy = value
        return value

    return option(
        "--deploy",
        is_flag=True,
        default=False,
        type=click_types.BOOL,
        help="Abort if the Pipfile.lock is out-of-date, or Python version is wrong.",
        callback=callback,
        expose_value=False,
    )(f)


def lock_only_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.lock_only = value
        return value

    return option(
        "--lock-only",
        is_flag=True,
        default=False,
        help="Only update lock file (specifiers not added to Pipfile).",
        callback=callback,
        type=click_types.BOOL,
        expose_value=False,
    )(f)


def setup_verbosity(ctx, param, value):
    if not value:
        return
    ctx.ensure_object(State).project.s.PIPENV_VERBOSITY = value


def validate_python_path(ctx, param, value):
    # Validating the Python path is complicated by accepting a number of
    # friendly options: the default will be boolean False to enable
    # autodetection but it may also be a value which will be searched in
    # the path or an absolute path. To report errors as early as possible
    # we'll report absolute paths which do not exist:
    if isinstance(value, (str, bytes)):
        path = Path(value)
        if path.is_absolute() and not path.is_file():
            raise BadParameter(f"Expected Python at path {value} does not exist")
    return value


def validate_bool_or_none(ctx, param, value):
    if value is not None:
        return click_types.BOOL(value)
    return False


def validate_pypi_mirror(ctx, param, value):
    if value and not is_valid_url(value):
        raise BadParameter(f"Invalid PyPI mirror URL: {value}")
    return value


def skip_lock_option(f):
    def callback(ctx, param, value):
        if value:
            err.print(
                "The flag --skip-lock has been reintroduced (but is not recommended).  "
                "Without the lock resolver it is difficult to manage multiple package indexes, and hash checking is not provided.  "
                "However it can help manage installs with current deficiencies in locking across platforms.",
                style="yellow bold",
            )
            state = ctx.ensure_object(State)
            state.installstate.skip_lock = value
        return value

    return option(
        "--skip-lock",
        is_flag=True,
        default=False,
        expose_value=True,
        envvar="PIPENV_SKIP_LOCK",
        help="Install from Pipfile bypassing lock mechanisms.",
        callback=callback,
        type=click_types.BOOL,
        show_envvar=True,
    )(f)


# OLD REMOVED COMMANDS THAT WE STILL DISPLAY HELP TEXT FOR WHEN USED #
def keep_outdated_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.keep_outdated = value
        if value:
            err.print(
                "The flag --keep-outdated has been removed.  "
                "The flag did not respect package resolver results and lead to inconsistent lock files.  "
                "Consider using the `pipenv upgrade` command to selectively upgrade packages.",
                style="yellow bold",
            )
            raise ValueError("The flag --keep-outdated flag has been removed.")
        return value

    return option(
        "--keep-outdated",
        is_flag=True,
        default=False,
        expose_value=False,
        callback=callback,
        type=click_types.BOOL,
        show_envvar=True,
        hidden=True,  # This hides the option from the help text.
    )(f)


def selective_upgrade_option(f):
    def callback(ctx, param, value):
        state = ctx.ensure_object(State)
        state.installstate.selective_upgrade = value
        if value:
            err.print(
                "The flag --selective-upgrade has been removed.  "
                "The flag was buggy and lead to inconsistent lock files.  "
                "Consider using the `pipenv upgrade` command to selectively upgrade packages.",
                style="yellow bold",
            )
            raise ValueError("The flag --selective-upgrade flag has been removed.")
        return value

    return option(
        "--selective-upgrade",
        is_flag=True,
        default=False,
        type=click_types.BOOL,
        help="Update specified packages.",
        callback=callback,
        expose_value=False,
    )(f)


def common_options(f):
    f = pypi_mirror_option(f)
    f = verbose_option(f)
    f = quiet_option(f)
    f = clear_option(f)
    f = python_option(f)
    return f


def install_base_options(f):
    f = common_options(f)
    f = pre_option(f)
    f = extra_pip_args(f)
    f = keep_outdated_option(f)  # Removed, but still displayed in help text.
    return f


def uninstall_options(f):
    f = install_base_options(f)
    f = categories_option(f)
    f = uninstall_dev_option(f)
    f = editable_option(f)
    f = package_arg(f)
    f = skip_lock_option(f)  # Removed, but still displayed in help text.
    return f


def lock_options(f):
    f = install_base_options(f)
    f = lock_dev_option(f)
    f = dev_only_flag(f)
    f = categories_option(f)
    return f


def sync_options(f):
    f = install_base_options(f)
    f = install_dev_option(f)
    f = categories_option(f)
    return f


def install_options(f):
    f = sync_options(f)
    f = index_option(f)
    f = requirementstxt_option(f)
    f = ignore_pipfile_option(f)
    f = editable_option(f)
    f = package_arg(f)
    f = skip_lock_option(f)  # Removed, but still display help text.
    f = selective_upgrade_option(f)  # Removed, but still display help text.
    return f


def upgrade_options(f):
    f = lock_only_option(f)
    return f


def general_options(f):
    f = common_options(f)
    f = site_packages_option(f)
    return f
