"""
Package support for OpenBSD

.. note::

    The package repository is configured on each host using ``/etc/installurl``
    from OpenBSD 6.1 onwards. Earlier releases relied on ``/etc/pkg.conf``.

.. versionchanged:: 2016.3.5

    Package versions on OpenBSD are not normally specified explicitly; instead
    packages may be available in multiple *flavors*, and *branches* which are
    specified by the format of the package name. This module allows you to use
    the same formatting as ``pkg_add(1)``, and will select the empty flavor and
    default branch by default. Examples:

    .. code-block:: yaml

      - rsync
      - vim--no_x11
      - ruby%2.3

"""

import copy
import logging
import re

import salt.utils.data
import salt.utils.versions
from salt.exceptions import CommandExecutionError, MinionError

log = logging.getLogger(__name__)

# FIXME: replace guesswork with `pkg_info -z` to correctly identify package
#        flavors and branches
__PKG_RE = re.compile("^((?:[^-]+|-(?![0-9]))+)-([0-9][^-]*)(?:-(.*))?$")

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


def __virtual__():
    """
    Set the virtual pkg module if the os is OpenBSD
    """
    if __grains__["os"] == "OpenBSD":
        return __virtualname__
    return (
        False,
        "The openbsdpkg execution module cannot be loaded: "
        "only available on OpenBSD systems.",
    )


def _list_pkgs_from_context(versions_as_list):
    """
    Use pkg list from __context__
    """
    if versions_as_list:
        return __context__["pkg.list_pkgs"]
    else:
        ret = copy.deepcopy(__context__["pkg.list_pkgs"])
        __salt__["pkg_resource.stringify"](ret)
        return ret


def list_pkgs(versions_as_list=False, **kwargs):
    """
    List the packages currently installed as a dict::

        {'<package_name>': '<version>'}

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.list_pkgs
    """
    versions_as_list = salt.utils.data.is_true(versions_as_list)
    # not yet implemented or not applicable
    if any(
        [salt.utils.data.is_true(kwargs.get(x)) for x in ("removed", "purge_desired")]
    ):
        return {}

    if "pkg.list_pkgs" in __context__ and kwargs.get("use_context", True):
        return _list_pkgs_from_context(versions_as_list)

    ret = {}
    cmd = "pkg_info -q -a"
    out = __salt__["cmd.run_stdout"](cmd, output_loglevel="trace")
    for line in out.splitlines():
        try:
            pkgname, pkgver, flavor = __PKG_RE.match(line).groups()
        except AttributeError:
            continue
        pkgname += f"--{flavor}" if flavor else ""
        __salt__["pkg_resource.add_pkg"](ret, pkgname, pkgver)

    __salt__["pkg_resource.sort_pkglist"](ret)
    __context__["pkg.list_pkgs"] = copy.deepcopy(ret)
    if not versions_as_list:
        __salt__["pkg_resource.stringify"](ret)
    return ret


def latest_version(*names, **kwargs):
    """
    Return the latest version of the named package available for upgrade or
    installation. If more than one package name is specified, a dict of
    name/version pairs is returned.

    If the latest version of a given package is already installed, an empty
    string will be returned for that package.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.latest_version <package name>
    """
    kwargs.pop("refresh", True)

    pkgs = list_pkgs()
    ret = {}
    # Initialize the dict with empty strings
    for name in names:
        ret[name] = ""

        # Query the repository for the package name
        cmd = f"pkg_info -Q {name}"
        out = __salt__["cmd.run_stdout"](
            cmd, python_shell=False, output_loglevel="trace"
        )

        # Since we can only query instead of request the specific package
        # we'll have to go through the returned list and find what we
        # were looking for.
        # Keep in mind the match may be flavored.
        for line in out.splitlines():
            try:
                pkgname, pkgver, flavor = __PKG_RE.match(line).groups()
            except AttributeError:
                continue

            match = re.match(r".*\(installed\)$", pkgver)
            if match:
                # Package is explicitly marked as installed already,
                # so skip any further comparison and move on to the
                # next package to compare (if provided).
                break

            # First check if we need to look for flavors before
            # looking at unflavored packages.
            if f"{pkgname}--{flavor}" == name:
                pkgname += f"--{flavor}"
            elif pkgname == name:
                pass
            else:
                # No match just move on.
                continue

            cur = pkgs.get(pkgname, "")
            if not cur or salt.utils.versions.compare(ver1=cur, oper="<", ver2=pkgver):
                ret[pkgname] = pkgver

    # Return a string if only one package name passed
    if len(names) == 1:
        return ret[names[0]]
    return ret


def version(*names, **kwargs):
    """
    Returns a string representing the package version or an empty string if not
    installed. If more than one package name is specified, a dict of
    name/version pairs is returned.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.version <package name>
        salt '*' pkg.version <package1> <package2> <package3> ...
    """
    return __salt__["pkg_resource.version"](*names, **kwargs)


def install(name=None, pkgs=None, sources=None, **kwargs):
    """
    Install the passed package

    Return a dict containing the new package names and versions::

        {'<package>': {'old': '<old-version>',
                       'new': '<new-version>'}}

    CLI Example, Install one package:

    .. code-block:: bash

        salt '*' pkg.install <package name>

    CLI Example, Install more than one package:

    .. code-block:: bash

        salt '*' pkg.install pkgs='["<package name>", "<package name>"]'

    CLI Example, Install more than one package from a alternate source (e.g.
    salt file-server, HTTP, FTP, local filesystem):

    .. code-block:: bash

        salt '*' pkg.install sources='[{"<pkg name>": "salt://pkgs/<pkg filename>"}]'
    """
    try:
        pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"](
            name, pkgs, sources, **kwargs
        )
    except MinionError as exc:
        raise CommandExecutionError(exc)

    if not pkg_params:
        return {}

    old = list_pkgs()
    errors = []
    for pkg in pkg_params:
        # A special case for OpenBSD package "branches" is also required in
        # salt/states/pkg.py
        if pkg_type == "repository":
            stem, branch = (pkg.split("%") + [""])[:2]
            base, flavor = (stem.split("--") + [""])[:2]
            pkg = f"{base}--{flavor}%{branch}"
        cmd = f"pkg_add -x -I {pkg}"
        out = __salt__["cmd.run_all"](cmd, python_shell=False, output_loglevel="trace")
        if out["retcode"] != 0 and out["stderr"]:
            errors.append(out["stderr"])

    __context__.pop("pkg.list_pkgs", None)
    new = list_pkgs()
    ret = salt.utils.data.compare_dicts(old, new)

    if errors:
        raise CommandExecutionError(
            "Problem encountered installing package(s)",
            info={"errors": errors, "changes": ret},
        )

    return ret


def remove(name=None, pkgs=None, purge=False, **kwargs):
    """
    Remove a single package with pkg_delete

    Multiple Package Options:

    pkgs
        A list of packages to delete. Must be passed as a python list. The
        ``name`` parameter will be ignored if this option is passed.

    .. versionadded:: 0.16.0


    Returns a dict containing the changes.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.remove <package name>
        salt '*' pkg.remove <package1>,<package2>,<package3>
        salt '*' pkg.remove pkgs='["foo", "bar"]'
    """
    try:
        pkg_params = [
            x.split("--")[0]
            for x in __salt__["pkg_resource.parse_targets"](name, pkgs)[0]
        ]
    except MinionError as exc:
        raise CommandExecutionError(exc)

    old = list_pkgs()
    targets = [x for x in pkg_params if x in old]
    if not targets:
        return {}

    cmd = ["pkg_delete", "-Ix", "-Ddependencies"]

    if purge:
        cmd.append("-cqq")

    cmd.extend(targets)

    out = __salt__["cmd.run_all"](cmd, python_shell=False, output_loglevel="trace")
    if out["retcode"] != 0 and out["stderr"]:
        errors = [out["stderr"]]
    else:
        errors = []

    __context__.pop("pkg.list_pkgs", None)
    new = list_pkgs()
    ret = salt.utils.data.compare_dicts(old, new)

    if errors:
        raise CommandExecutionError(
            "Problem encountered removing package(s)",
            info={"errors": errors, "changes": ret},
        )

    return ret


def purge(name=None, pkgs=None, **kwargs):
    """
    Remove a package and extra configuration files.

    name
        The name of the package to be deleted.


    Multiple Package Options:

    pkgs
        A list of packages to delete. Must be passed as a python list. The
        ``name`` parameter will be ignored if this option is passed.

    .. versionadded:: 0.16.0


    Returns a dict containing the changes.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.purge <package name>
        salt '*' pkg.purge <package1>,<package2>,<package3>
        salt '*' pkg.purge pkgs='["foo", "bar"]'
    """
    return remove(name=name, pkgs=pkgs, purge=True)


def upgrade_available(name, **kwargs):
    """
    Check whether or not an upgrade is available for a given package

    .. versionadded:: 2019.2.0

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.upgrade_available <package name>
    """
    return latest_version(name) != ""


def upgrade(name=None, pkgs=None, **kwargs):
    """
    Run a full package upgrade (``pkg_add -u``), or upgrade a specific package
    if ``name`` or ``pkgs`` is provided.
    ``name`` is ignored when ``pkgs`` is specified.

    Returns a dictionary containing the changes:

    .. versionadded:: 2019.2.0

    .. code-block:: python

        {'<package>': {'old': '<old-version>',
                       'new': '<new-version>'}}

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.upgrade
        salt '*' pkg.upgrade python%2.7
    """
    old = list_pkgs()

    cmd = ["pkg_add", "-Ix", "-u"]

    if kwargs.get("noop", False):
        cmd.append("-n")

    if pkgs:
        cmd.extend(pkgs)
    elif name:
        cmd.append(name)

    # Now run the upgrade, compare the list of installed packages before and
    # after and we have all the info we need.
    result = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)

    __context__.pop("pkg.list_pkgs", None)
    new = list_pkgs()
    ret = salt.utils.data.compare_dicts(old, new)

    if result["retcode"] != 0:
        raise CommandExecutionError(
            "Problem encountered upgrading packages",
            info={"changes": ret, "result": result},
        )

    return ret
