# -*- coding: utf-8 -*-
'''
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

'''
from __future__ import absolute_import, print_function, unicode_literals

# Import python libs
import copy
import re
import logging

# Import Salt libs
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(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__:
        if versions_as_list:
            return __context__['pkg.list_pkgs']
        else:
            ret = copy.deepcopy(__context__['pkg.list_pkgs'])
            __salt__['pkg_resource.stringify'](ret)
            return ret

    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 += '--{0}'.format(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 = 'pkg_info -Q {0}'.format(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 "{0}--{1}".format(pkgname, flavor) == name:
                pkgname += '--{0}'.format(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 pkg_params is None or len(pkg_params) == 0:
        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 = '{0}--{1}%{2}'.format(base, flavor, branch)
        cmd = 'pkg_add -x -I {0}'.format(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):
    '''
    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
