"""
Setup of Python virtualenv sandboxes.

.. versionadded:: 0.17.0

.. note::

    This state module is loaded under the ``virtualenv`` virtual name. Use
    ``virtualenv.managed`` (and not ``virtualenv_mod.managed``) in your
    state SLS files.
"""

import logging
import os

import salt.utils.functools
import salt.utils.platform
import salt.utils.versions
import salt.version
from salt.exceptions import CommandExecutionError, CommandNotFoundError

log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = "virtualenv"


def __virtual__():
    if "virtualenv.create" in __salt__:
        return __virtualname__
    return (False, "virtualenv module could not be loaded")


def managed(
    name,
    venv_bin=None,
    requirements=None,
    system_site_packages=False,
    distribute=False,
    use_wheel=False,
    clear=False,
    python=None,
    extra_search_dir=None,
    never_download=None,
    prompt=None,
    user=None,
    cwd=None,
    index_url=None,
    extra_index_url=None,
    pre_releases=False,
    no_deps=False,
    pip_download=None,
    pip_download_cache=None,
    pip_exists_action=None,
    pip_ignore_installed=False,
    proxy=None,
    use_vt=False,
    env_vars=None,
    no_use_wheel=False,
    pip_upgrade=False,
    pip_pkgs=None,
    pip_no_cache_dir=False,
    pip_cache_dir=None,
    process_dependency_links=False,
    no_binary=None,
    **kwargs,
):
    """
    Create a virtualenv and optionally manage it with pip

    name
        Path to the virtualenv.

    venv_bin: virtualenv
        The name (and optionally path) of the virtualenv command. This can also
        be set globally in the minion config file as ``virtualenv.venv_bin``.
        The special value ``venv`` selects the python standard library
        ``venv`` module instead of a virtualenv binary; a python interpreter
        (e.g. ``/usr/bin/python3.11``) may also be given, in which case the
        environment is created with ``<interpreter> -m venv``.

        .. versionchanged:: 3006.28
            A python interpreter is now accepted as ``venv_bin``.

    requirements: None
        Path to a pip requirements file. If the path begins with ``salt://``
        the file will be transferred from the master file server.

    use_wheel: False
        Prefer wheel archives (requires pip >= 1.4).

    python: None
        Python executable used to build the virtualenv. When Salt is installed
        from a onedir package. You will likely want to specify which python
        interperter should be used.

        .. versionchanged:: 3006.28
            Also honoured with ``venv_bin: venv``: the environment is created
            by running ``<python> -m venv``, so distros whose virtualenv
            binary is outdated (e.g. EL8) can still build environments for
            any installed interpreter.

    user: None
        The user under which to run virtualenv and pip.

    cwd: None
        Path to the working directory where `pip install` is executed.

    no_deps: False
        Pass `--no-deps` to `pip install`.

    pip_exists_action: None
        Default action of pip when a path already exists: (s)witch, (i)gnore,
        (w)ipe, (b)ackup.

    proxy: None
        Proxy address which is passed to `pip install`.

    env_vars: None
        Set environment variables that some builds will depend on. For example,
        a Python C-module may have a Makefile that needs INCLUDE_PATH set to
        pick up a header file while compiling.

    no_use_wheel: False
        Force to not use wheel archives (requires pip>=1.4)

    no_binary
        Force to not use binary packages (requires pip >= 7.0.0)
        Accepts either :all: to disable all binary packages, :none: to empty the set,
        or a list of one or more packages

    pip_upgrade: False
        Pass `--upgrade` to `pip install`.

    pip_pkgs: None
        As an alternative to `requirements`, pass a list of pip packages that
        should be installed.

    process_dependency_links: False
        Run pip install with the --process_dependency_links flag.

        .. versionadded:: 2017.7.0

    Also accepts any keyword argument accepted by
    :py:func:`virtualenv.create <salt.modules.virtualenv_mod.create>` --
    including ``system_site_packages``, ``distribute``, ``clear``,
    ``extra_search_dir``, ``never_download``, ``prompt``, ``index_url``,
    ``extra_index_url``, ``pre_releases``, ``pip_download``,
    ``pip_download_cache``, ``pip_ignore_installed``, ``use_vt``,
    ``pip_no_cache_dir`` and ``pip_cache_dir``. Refer to that execution
    module for argument semantics. Some kwargs, such as the ``pip`` option,
    require ``- distribute: True``.

    .. code-block:: yaml

        /var/www/myvirtualenv.com:
          virtualenv.managed:
            - system_site_packages: False
            - requirements: salt://REQUIREMENTS.txt
            - env_vars:
                PATH_VAR: '/usr/local/bin/'

    Current versions of Salt use onedir packages and will use onedir python
    interpreter by default. If you've installed Salt via out package
    repository. You will likely want to provide the path to the interpreter
    with which you would like to be used to create the virtual environment. The
    interpreter can be specified by providing the `python` option.
    """
    ret = {"name": name, "result": True, "comment": "", "changes": {}}

    if "virtualenv.create" not in __salt__:
        ret["result"] = False
        ret["comment"] = "Virtualenv was not detected on this system"
        return ret

    if salt.utils.platform.is_windows():
        venv_py = os.path.join(name, "Scripts", "python.exe")
    else:
        venv_py = os.path.join(name, "bin", "python")
    venv_exists = os.path.exists(venv_py)

    # Bail out early if the specified requirements file can't be found
    if requirements and requirements.startswith("salt://"):
        cached_requirements = __salt__["cp.is_cached"](requirements, __env__)
        if not cached_requirements:
            # It's not cached, let's cache it.
            cached_requirements = __salt__["cp.cache_file"](requirements, __env__)
        # Check if the master version has changed.
        if cached_requirements and __salt__["cp.hash_file"](
            requirements, __env__
        ) != __salt__["cp.hash_file"](cached_requirements, __env__):
            cached_requirements = __salt__["cp.cache_file"](requirements, __env__)
        if not cached_requirements:
            ret.update(
                {
                    "result": False,
                    "comment": "pip requirements file '{}' not found".format(
                        requirements
                    ),
                }
            )
            return ret
        requirements = cached_requirements

    # If it already exists, grab the version for posterity
    if venv_exists and clear:
        ret["changes"]["cleared_packages"] = __salt__["pip.freeze"](bin_env=name)
        ret["changes"]["old"] = __salt__["cmd.run_stderr"](f"{venv_py} -V").strip("\n")

    # Create (or clear) the virtualenv
    if __opts__["test"]:
        if venv_exists and clear:
            ret["result"] = None
            ret["comment"] = f"Virtualenv {name} is set to be cleared"
            return ret
        if venv_exists and not clear:
            ret["comment"] = f"Virtualenv {name} is already created"
            return ret
        ret["result"] = None
        ret["comment"] = f"Virtualenv {name} is set to be created"
        return ret

    if not venv_exists or (venv_exists and clear):
        try:
            venv_ret = __salt__["virtualenv.create"](
                name,
                venv_bin=venv_bin,
                system_site_packages=system_site_packages,
                distribute=distribute,
                clear=clear,
                python=python,
                extra_search_dir=extra_search_dir,
                never_download=never_download,
                prompt=prompt,
                user=user,
                use_vt=use_vt,
                **kwargs,
            )
        except CommandNotFoundError as err:
            ret["result"] = False
            ret["comment"] = f"Failed to create virtualenv: {err}"
            return ret

        if venv_ret["retcode"] != 0:
            ret["result"] = False
            ret["comment"] = venv_ret["stdout"] + venv_ret["stderr"]
            return ret

        ret["result"] = True
        ret["changes"]["new"] = __salt__["cmd.run_stderr"](f"{venv_py} -V").strip("\n")

        if clear:
            ret["comment"] = "Cleared existing virtualenv"
        else:
            ret["comment"] = "Created new virtualenv"

    elif venv_exists:
        ret["comment"] = "virtualenv exists"

    # Check that the pip binary supports the 'use_wheel' option
    if use_wheel:
        min_version = "1.4"
        max_version = "9.0.3"
        cur_version = __salt__["pip.version"](bin_env=name)
        too_low = salt.utils.versions.compare(
            ver1=cur_version, oper="<", ver2=min_version
        )
        too_high = salt.utils.versions.compare(
            ver1=cur_version, oper=">", ver2=max_version
        )
        if too_low or too_high:
            ret["result"] = False
            ret["comment"] = (
                "The 'use_wheel' option is only supported in "
                "pip between {} and {}. The version of pip detected "
                "was {}.".format(min_version, max_version, cur_version)
            )
            return ret

    # Check that the pip binary supports the 'no_use_wheel' option
    if no_use_wheel:
        min_version = "1.4"
        max_version = "9.0.3"
        cur_version = __salt__["pip.version"](bin_env=name)
        too_low = salt.utils.versions.compare(
            ver1=cur_version, oper="<", ver2=min_version
        )
        too_high = salt.utils.versions.compare(
            ver1=cur_version, oper=">", ver2=max_version
        )
        if too_low or too_high:
            ret["result"] = False
            ret["comment"] = (
                "The 'no_use_wheel' option is only supported in "
                "pip between {} and {}. The version of pip detected "
                "was {}.".format(min_version, max_version, cur_version)
            )
            return ret

    # Check that the pip binary supports the 'no_binary' option
    if no_binary:
        min_version = "7.0.0"
        cur_version = __salt__["pip.version"](bin_env=name)
        too_low = salt.utils.versions.compare(
            ver1=cur_version, oper="<", ver2=min_version
        )
        if too_low:
            ret["result"] = False
            ret["comment"] = (
                "The 'no_binary' option is only supported in "
                "pip {} and newer. The version of pip detected "
                "was {}.".format(min_version, cur_version)
            )
            return ret

    # Populate the venv via a requirements file
    if requirements or pip_pkgs:
        try:
            before = set(__salt__["pip.freeze"](bin_env=name, user=user, use_vt=use_vt))
        except CommandExecutionError as exc:
            ret["result"] = False
            ret["comment"] = exc.strerror
            return ret

        if requirements:

            if isinstance(requirements, str):
                req_canary = requirements.split(",")[0]
            elif isinstance(requirements, list):
                req_canary = requirements[0]
            else:
                raise TypeError("pip requirements must be either a string or a list")

            if req_canary != os.path.abspath(req_canary):
                cwd = os.path.dirname(os.path.abspath(req_canary))

        pip_ret = __salt__["pip.install"](
            pkgs=pip_pkgs,
            requirements=requirements,
            process_dependency_links=process_dependency_links,
            bin_env=name,
            use_wheel=use_wheel,
            no_use_wheel=no_use_wheel,
            no_binary=no_binary,
            user=user,
            cwd=cwd,
            index_url=index_url,
            extra_index_url=extra_index_url,
            download=pip_download,
            download_cache=pip_download_cache,
            pre_releases=pre_releases,
            exists_action=pip_exists_action,
            ignore_installed=pip_ignore_installed,
            upgrade=pip_upgrade,
            no_deps=no_deps,
            proxy=proxy,
            use_vt=use_vt,
            env_vars=env_vars,
            no_cache_dir=pip_no_cache_dir,
            cache_dir=pip_cache_dir,
            **kwargs,
        )
        ret["result"] &= pip_ret["retcode"] == 0
        if pip_ret["retcode"] > 0:
            ret["comment"] = "{}\n{}\n{}".format(
                ret["comment"], pip_ret["stdout"], pip_ret["stderr"]
            )

        after = set(__salt__["pip.freeze"](bin_env=name))

        new = list(after - before)
        old = list(before - after)

        if new or old:
            ret["changes"]["packages"] = {
                "new": new if new else "",
                "old": old if old else "",
            }
    return ret


manage = salt.utils.functools.alias_function(managed, "manage")
