import contextlib
import os
import re
import shutil
import sys
from pathlib import Path

from pipenv import environments, exceptions
from pipenv.patched.pip._vendor import rich
from pipenv.utils.dependencies import python_version
from pipenv.utils.environment import ensure_environment
from pipenv.utils.processes import subprocess_run
from pipenv.utils.shell import find_python, shorten_path
from pipenv.vendor import click

console = rich.console.Console()
err = rich.console.Console(stderr=True)


def warn_in_virtualenv(project):
    # Only warn if pipenv isn't already active.
    if environments.is_in_virtualenv() and not project.s.is_quiet():
        click.echo(
            "{}: Pipenv found itself running within a virtual environment, "
            "so it will automatically use that environment, instead of "
            "creating its own for any project. You can set "
            "{} to force pipenv to ignore that environment and create "
            "its own instead. You can set {} to suppress this "
            "warning.".format(
                click.style("Courtesy Notice", fg="green"),
                click.style("PIPENV_IGNORE_VIRTUALENVS=1", bold=True),
                click.style("PIPENV_VERBOSITY=-1", bold=True),
            ),
            err=True,
        )


def do_create_virtualenv(project, python=None, site_packages=None, pypi_mirror=None):
    """Creates a virtualenv."""

    click.secho("Creating a virtualenv for this project...", bold=True, err=True)

    click.echo(
        "Pipfile: " + click.style(project.pipfile_location, fg="yellow", bold=True),
        err=True,
    )

    # Default to using sys.executable, if Python wasn't provided.
    using_string = "Using"
    if not python:
        python = sys.executable
        using_string = "Using default python from"
    click.echo(
        "{} {} {} {}".format(
            click.style(using_string, bold=True),
            click.style(python, fg="yellow", bold=True),
            click.style(f"({python_version(python)})", fg="green"),
            click.style("to create virtualenv...", bold=True),
        ),
        err=True,
    )

    if site_packages:
        click.secho("Making site-packages available...", bold=True, err=True)

    if pypi_mirror:
        pip_config = {"PIP_INDEX_URL": pypi_mirror}
    else:
        pip_config = {}

    error = None
    with console.status(
        "Creating virtual environment...", spinner=project.s.PIPENV_SPINNER
    ):
        cmd = _create_virtualenv_cmd(project, python, site_packages=site_packages)
        c = subprocess_run(cmd, env=pip_config)
        click.secho(f"{c.stdout}", fg="cyan", err=True)
        if c.returncode != 0:
            error = (
                c.stderr if project.s.is_verbose() else exceptions.prettify_exc(c.stderr)
            )
            err.print(
                environments.PIPENV_SPINNER_FAIL_TEXT.format(
                    "Failed creating virtual environment"
                )
            )
        else:
            err.print(
                environments.PIPENV_SPINNER_OK_TEXT.format(
                    "Successfully created virtual environment!"
                )
            )
    if error is not None:
        raise exceptions.VirtualenvCreationException(
            extra=click.style(f"{error}", fg="red")
        )

    # Associate project directory with the environment.
    project_file_name = os.path.join(project.virtualenv_location, ".project")
    with open(project_file_name, "w") as f:
        f.write(project.project_directory)
    from pipenv.environment import Environment

    sources = project.pipfile_sources()
    # project.get_location_for_virtualenv is only for if we are creating a new virtualenv
    # whereas virtualenv_location is for the current path to the runtime
    project._environment = Environment(
        prefix=project.virtualenv_location,
        is_venv=True,
        sources=sources,
        pipfile=project.parsed_pipfile,
        project=project,
    )
    # Say where the virtualenv is.
    do_where(project, virtualenv=True, bare=False)


def _create_virtualenv_cmd(project, python, site_packages=False):
    cmd = [
        Path(sys.executable).absolute().as_posix(),
        "-m",
        "virtualenv",
    ]
    if project.s.PIPENV_VIRTUALENV_CREATOR:
        cmd.append(f"--creator={project.s.PIPENV_VIRTUALENV_CREATOR}")
    cmd.append(f"--prompt={project.name}")
    cmd.append(f"--python={python}")
    cmd.append(project.get_location_for_virtualenv())
    if project.s.PIPENV_VIRTUALENV_COPIES:
        cmd.append("--copies")

    # Pass site-packages flag to virtualenv, if desired...
    if site_packages:
        cmd.append("--system-site-packages")

    return cmd


def ensure_virtualenv(project, python=None, site_packages=None, pypi_mirror=None):
    """Creates a virtualenv, if one doesn't exist."""

    def abort():
        sys.exit(1)

    if not project.virtualenv_exists:
        try:
            # Ensure environment variables are set properly.
            ensure_environment()
            # Ensure Python is available.
            python = ensure_python(project, python=python)
            if python is not None and not isinstance(python, str):
                python = python.path.as_posix()
            # Create the virtualenv.
            # Abort if --system (or running in a virtualenv).
            if project.s.PIPENV_USE_SYSTEM:
                click.secho(
                    "You are attempting to re–create a virtualenv that "
                    "Pipenv did not create. Aborting.",
                    fg="red",
                )
                sys.exit(1)
            do_create_virtualenv(
                project,
                python=python,
                site_packages=site_packages,
                pypi_mirror=pypi_mirror,
            )
        except KeyboardInterrupt:
            # If interrupted, cleanup the virtualenv.
            cleanup_virtualenv(project, bare=False)
            sys.exit(1)
    # If --python or was passed...
    elif (python) or (site_packages is not None):
        project.s.USING_DEFAULT_PYTHON = False
        # Ensure python is installed before deleting existing virtual env
        python = ensure_python(project, python=python)
        if python is not None and not isinstance(python, str):
            python = python.path.as_posix()

        click.secho("Virtualenv already exists!", fg="red", err=True)
        # If VIRTUAL_ENV is set, there is a possibility that we are
        # going to remove the active virtualenv that the user cares
        # about, so confirm first.
        if "VIRTUAL_ENV" in os.environ and not (
            project.s.PIPENV_YES
            or click.confirm("Use existing virtualenv?", default=True)
        ):
            abort()
        click.echo(click.style("Using existing virtualenv...", bold=True), err=True)
        # Remove the virtualenv.
        cleanup_virtualenv(project, bare=True)
        # Call this function again.
        ensure_virtualenv(
            project,
            python=python,
            site_packages=site_packages,
            pypi_mirror=pypi_mirror,
        )


def cleanup_virtualenv(project, bare=True):
    """Removes the virtualenv directory from the system."""
    if not bare:
        click.secho("Environment creation aborted.", fg="red")
    try:
        # Delete the virtualenv.
        shutil.rmtree(project.virtualenv_location)
    except OSError as e:
        click.echo(
            "{} An error occurred while removing {}!".format(
                click.style("Error: ", fg="red", bold=True),
                click.style(project.virtualenv_location, fg="green"),
            ),
            err=True,
        )
        click.secho(e, fg="cyan", err=True)


def ensure_python(project, python=None):
    # Runtime import is necessary due to the possibility that the environments module may have been reloaded.
    if project.s.PIPENV_PYTHON and not python:
        python = project.s.PIPENV_PYTHON

    def abort(msg=""):
        click.echo(
            "{}\nYou can specify specific versions of Python with:\n{}".format(
                click.style(msg, fg="red"),
                click.style(
                    "$ pipenv --python {}".format(os.sep.join(("path", "to", "python"))),
                    fg="yellow",
                ),
            ),
            err=True,
        )
        sys.exit(1)

    project.s.USING_DEFAULT_PYTHON = not python
    # Find out which python is desired.
    if not python:
        python = project.required_python_version
        if python:
            range_pattern = r"^[<>]=?|!="
            if re.search(range_pattern, python):
                err.print(
                    f"[bold red]Error[/bold red]: Python version range specifier '[cyan]{python}[/cyan]' is not supported. "
                    "[yellow]Please use an absolute version number or specify the path to the Python executable on Pipfile.[/yellow]"
                )
                sys.exit(1)

    if not python:
        python = project.s.PIPENV_DEFAULT_PYTHON_VERSION
    path_to_python = find_a_system_python(python)
    if project.s.is_verbose():
        click.echo(f"Using python: {python}", err=True)
        click.echo(f"Path to python: {path_to_python}", err=True)
    if not path_to_python and python is not None:
        # We need to install Python.
        click.echo(
            "{}: Python {} {}".format(
                click.style("Warning", fg="red", bold=True),
                click.style(python, fg="cyan"),
                "was not found on your system...",
            ),
            err=True,
        )
        # check for python installers
        from pipenv.installers import Asdf, InstallerError, InstallerNotFound, Pyenv

        # prefer pyenv if both pyenv and asdf are installed as it's
        # dedicated to python installs so probably the preferred
        # method of the user for new python installs.
        installer = None
        if not project.s.PIPENV_DONT_USE_PYENV:
            with contextlib.suppress(InstallerNotFound):
                installer = Pyenv(project)

        if installer is None and not project.s.PIPENV_DONT_USE_ASDF:
            with contextlib.suppress(InstallerNotFound):
                installer = Asdf(project)

        if not installer:
            abort("Neither 'pyenv' nor 'asdf' could be found to install Python.")
        else:
            if environments.SESSION_IS_INTERACTIVE or project.s.PIPENV_YES:
                try:
                    version = installer.find_version_to_install(python)
                except ValueError:
                    abort()
                except InstallerError as e:
                    abort(f"Something went wrong while installing Python:\n{e.err}")
                s = "{} {} {}".format(
                    "Would you like us to install",
                    click.style(f"CPython {version}", fg="green"),
                    f"with {installer}?",
                )
                # Prompt the user to continue...
                if not (project.s.PIPENV_YES or click.confirm(s, default=True)):
                    abort()
                else:
                    # Tell the user we're installing Python.
                    click.echo(
                        "{} {} {} {}{}".format(
                            click.style("Installing", bold=True),
                            click.style(f"CPython {version}", bold=True, fg="green"),
                            click.style(f"with {installer.cmd}", bold=True),
                            click.style("(this may take a few minutes)"),
                            click.style("...", bold=True),
                        )
                    )
                    with console.status(
                        "Installing python...", spinner=project.s.PIPENV_SPINNER
                    ):
                        try:
                            c = installer.install(version)
                        except InstallerError as e:
                            err.print(
                                environments.PIPENV_SPINNER_FAIL_TEXT.format("Failed...")
                            )
                            click.echo("Something went wrong...", err=True)
                            click.secho(e.err, fg="cyan", err=True)
                        else:
                            console.print(
                                environments.PIPENV_SPINNER_OK_TEXT.format("Success!")
                            )
                            # Print the results, in a beautiful blue...
                            click.secho(c.stdout, fg="cyan", err=True)
                    # Find the newly installed Python, hopefully.
                    version = str(version)
                    path_to_python = find_a_system_python(version)
                    try:
                        assert python_version(path_to_python) == version
                    except AssertionError:
                        click.echo(
                            "{}: The Python you just installed is not available on your {}, apparently."
                            "".format(
                                click.style("Warning", fg="red", bold=True),
                                click.style("PATH", bold=True),
                            ),
                            err=True,
                        )
                        sys.exit(1)
    return path_to_python


def find_a_system_python(line):
    """Find a Python installation from a given line.

    This tries to parse the line in various of ways:

    * Looks like an absolute path? Use it directly.
    * Looks like a py.exe call? Use py.exe to get the executable.
    * Starts with "py" something? Looks like a python command. Try to find it
      in PATH, and use it directly.
    * Search for "python" and "pythonX.Y" executables in PATH to find a match.
    * Nothing fits, return None.
    """

    from pipenv.vendor.pythonfinder import Finder

    finder = Finder(system=True, global_search=True)
    if not line:
        return next(iter(finder.find_all_python_versions()), None)
    # Use the windows finder executable
    if (line.startswith(("py ", "py.exe "))) and os.name == "nt":
        line = line.split(" ", 1)[1].lstrip("-")
    python_entry = find_python(finder, line)
    return python_entry


def do_where(project, virtualenv=False, bare=True):
    """Executes the where functionality."""
    if not virtualenv:
        if not project.pipfile_exists:
            click.echo(
                "No Pipfile present at project home. Consider running "
                "{} first to automatically generate a Pipfile for you."
                "".format(click.style("`pipenv install`", fg="green")),
                err=True,
            )
            return
        location = project.pipfile_location
        # Shorten the virtual display of the path to the virtualenv.
        if not bare:
            location = shorten_path(location)
            click.echo(
                "Pipfile found at {}.\n  Considering this to be the project home."
                "".format(click.style(location, fg="green")),
                err=True,
            )
        else:
            click.echo(project.project_directory)
    else:
        location = project.virtualenv_location
        if not bare:
            click.secho(f"Virtualenv location: {location}", fg="green", err=True)
        else:
            click.echo(location)


def inline_activate_virtual_environment(project):
    root = project.virtualenv_location
    if os.path.exists(os.path.join(root, "pyvenv.cfg")):
        _inline_activate_venv(project)
    else:
        _inline_activate_virtualenv(project)
    if "VIRTUAL_ENV" not in os.environ:
        os.environ["VIRTUAL_ENV"] = root


def _inline_activate_venv(project):
    """Built-in venv doesn't have activate_this.py, but doesn't need it anyway.

    As long as we find the correct executable, built-in venv sets up the
    environment automatically.

    See: https://bugs.python.org/issue21496#msg218455
    """
    components = []
    for name in ("bin", "Scripts"):
        bindir = os.path.join(project.virtualenv_location, name)
        if os.path.exists(bindir):
            components.append(bindir)
    if "PATH" in os.environ:
        components.append(os.environ["PATH"])
    os.environ["PATH"] = os.pathsep.join(components)


def _inline_activate_virtualenv(project):
    try:
        activate_this = project._which("activate_this.py")
        if not activate_this or not os.path.exists(activate_this):
            raise exceptions.VirtualenvActivationException()
        with open(activate_this) as f:
            code = compile(f.read(), activate_this, "exec")
            exec(code, {"__file__": activate_this})
    # Catch all errors, just in case.
    except Exception:
        click.echo(
            "{}: There was an unexpected error while activating your "
            "virtualenv. Continuing anyway...".format(
                click.style("Warning", fg="red", bold=True)
            ),
            err=True,
        )
