# -*- coding: utf-8 -*-
"""
Setup of Python virtualenv sandboxes.

.. versionadded:: 0.17.0
"""

# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals

import logging
import os

import salt.utils.functools
import salt.utils.platform
import salt.utils.versions

# Import Salt libs
import salt.version
from salt.exceptions import CommandExecutionError, CommandNotFoundError

# Import 3rd-party libs
from salt.ext import six

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``.

    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

    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 kwargs that the virtualenv module will. However, 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/'
    """
    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 '{0}' 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"](
            "{0} -V".format(venv_py)
        ).strip("\n")

    # Create (or clear) the virtualenv
    if __opts__["test"]:
        if venv_exists and clear:
            ret["result"] = None
            ret["comment"] = "Virtualenv {0} is set to be cleared".format(name)
            return ret
        if venv_exists and not clear:
            ret["comment"] = "Virtualenv {0} is already created".format(name)
            return ret
        ret["result"] = None
        ret["comment"] = "Virtualenv {0} is set to be created".format(name)
        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"] = "Failed to create virtualenv: {0}".format(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"](
            "{0} -V".format(venv_py)
        ).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 {0} and {1}. The version of pip detected "
                "was {2}."
            ).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 {0} and {1}. The version of pip detected "
                "was {2}."
            ).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 {0} and newer. The version of pip detected "
                "was {1}."
            ).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, six.string_types):
                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"] = "{0}\n{1}\n{2}".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")
