# -*- coding: utf-8 -*-
'''
Package support for XBPS package manager (used by VoidLinux)

.. versionadded:: 2016.11.0
'''

# TODO: what about the initial acceptance of repo's fingerprint when adding a
# new repo?

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import os
import re
import logging
import glob

# Import salt libs
import salt.utils.data
import salt.utils.files
import salt.utils.path
import salt.utils.pkg
import salt.utils.stringutils
import salt.utils.decorators as decorators
from salt.exceptions import CommandExecutionError, MinionError

log = logging.getLogger(__name__)

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


def __virtual__():
    '''
    Set the virtual pkg module if the os is Void and xbps-install found
    '''
    if __grains__['os'] in ('Void') and _check_xbps():
        return __virtualname__
    return False


@decorators.memoize
def _check_xbps():
    '''
    Looks to see if xbps-install is present on the system, return full path
    '''
    return salt.utils.path.which('xbps-install')


@decorators.memoize
def _get_version():
    '''
    Get the xbps version
    '''
    version_string = __salt__['cmd.run'](
        [_check_xbps(), '--version'],
        output_loglevel='trace')
    if version_string is None:
        # Dunno why it would, but...
        return False

    VERSION_MATCH = re.compile(r'(?:XBPS:[\s]+)([\d.]+)(?:[\s]+.*)')
    version_match = VERSION_MATCH.search(version_string)
    if not version_match:
        return False

    return version_match.group(1).split('.')


def _rehash():
    '''
    Recomputes internal hash table for the PATH variable.
    Used whenever a new command is created during the current
    session.
    '''
    shell = __salt__['environ.get']('SHELL')
    if shell.split('/')[-1] in ('csh', 'tcsh'):
        __salt__['cmd.run']('rehash', output_loglevel='trace')


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 {}

    cmd = 'xbps-query -l'
    ret = {}
    out = __salt__['cmd.run'](cmd, output_loglevel='trace')
    for line in out.splitlines():
        if not line:
            continue
        try:
            # xbps-query -l output sample:
            # ii desktop-file-utils-0.22_4  Utilities to ...
            #
            # XXX handle package status (like 'ii') ?
            pkg, ver = line.split(None)[1].rsplit('-', 1)
        except ValueError:
            log.error(
                'xbps-query: Unexpected formatting in line: "%s"',
                line
            )

        __salt__['pkg_resource.add_pkg'](ret, pkg, ver)

    __salt__['pkg_resource.sort_pkglist'](ret)
    if not versions_as_list:
        __salt__['pkg_resource.stringify'](ret)
    return ret


def list_upgrades(refresh=True):
    '''
    Check whether or not an upgrade is available for all packages

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.list_upgrades
    '''

    # sample output of 'xbps-install -un':
    #     fuse-2.9.4_4 update i686 http://repo.voidlinux.eu/current 298133 91688
    #     xtools-0.34_1 update noarch http://repo.voidlinux.eu/current 21424 10752

    refresh = salt.utils.data.is_true(refresh)

    # Refresh repo index before checking for latest version available
    if refresh:
        refresh_db()

    ret = {}

    # retrieve list of updatable packages
    cmd = 'xbps-install -un'
    out = __salt__['cmd.run'](cmd, output_loglevel='trace')
    for line in out.splitlines():
        if not line:
            continue
        pkg = "base-system"
        ver = "NonNumericValueIsError"
        try:
            pkg, ver = line.split()[0].rsplit('-', 1)
        except (ValueError, IndexError):
            log.error(
                'xbps-query: Unexpected formatting in line: "%s"',
                line
            )
            continue

        log.trace('pkg=%s version=%s', pkg, ver)
        ret[pkg] = ver

    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>
        salt '*' pkg.latest_version <package1> <package2> <package3> ...
    '''

    # Why using 'xbps-install -un' and not 'xbps-query -R':
    # if several repos, xbps-query will produces this kind of output,
    # that is difficult to handle correctly:
    #     [*] salt-2015.8.3_2 Remote execution system ...
    #     [-] salt-2015.8.3_1 Remote execution system ...
    #
    # XXX 'xbps-install -un pkg1 pkg2' won't produce any info on updatable pkg1
    #     if pkg2 is up-to-date. Bug of xbps 0.51, probably get fixed in 0.52.
    #     See related issue https://github.com/voidlinux/xbps/issues/145
    #
    # sample outputs of 'xbps-install -un':
    #     fuse-2.9.4_4 update i686 http://repo.voidlinux.eu/current 298133 91688
    #     xtools-0.34_1 update noarch http://repo.voidlinux.eu/current 21424 10752
    #     Package 'vim' is up to date.

    refresh = salt.utils.data.is_true(kwargs.pop('refresh', True))

    if len(names) == 0:
        return ''

    # Refresh repo index before checking for latest version available
    if refresh:
        refresh_db()

    # Initialize the dict with empty strings
    ret = {}
    for name in names:
        ret[name] = ''

    # retrieve list of updatable packages
    # ignore return code since 'is up to date' case produces retcode==17 (xbps 0.51)
    cmd = ['xbps-install', '-un']
    cmd.extend(names)
    out = __salt__['cmd.run'](cmd, ignore_retcode=True, output_loglevel='trace')
    for line in out.splitlines():
        if not line:
            continue
        if line.find(' is up to date.') != -1:
            continue
        # retrieve tuple pkgname version
        try:
            pkg, ver = line.split()[0].rsplit('-', 1)
        except (ValueError, IndexError):
            log.error(
                'xbps-query: Unexpected formatting in line: "%s"',
                line
            )
            continue

        log.trace('pkg=%s version=%s', pkg, ver)
        if pkg in names:
            ret[pkg] = ver

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


# available_version is being deprecated
available_version = latest_version


def upgrade_available(name):
    '''
    Check whether or not an upgrade is available for a given package

    CLI Example:

    .. code-block:: bash

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


def refresh_db():
    '''
    Update list of available packages from installed repos

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.refresh_db
    '''
    # Remove rtag file to keep multiple refreshes from happening in pkg states
    salt.utils.pkg.clear_rtag(__opts__)
    cmd = 'xbps-install -Sy'
    call = __salt__['cmd.run_all'](cmd, output_loglevel='trace')
    if call['retcode'] != 0:
        comment = ''
        if 'stderr' in call:
            comment += call['stderr']

        raise CommandExecutionError(comment)

    return True


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 upgrade(refresh=True):
    '''
    Run a full system upgrade

    refresh
        Whether or not to refresh the package database before installing.
        Default is `True`.

    Returns a dictionary containing the changes:

    .. code-block:: python

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


    CLI Example:

    .. code-block:: bash

        salt '*' pkg.upgrade
    '''

    # XXX if xbps has to be upgraded, 2 times is required to fully upgrade
    # system: one for xbps, a subsequent one for all other packages. Not
    # handled in this code.

    old = list_pkgs()

    cmd = ['xbps-install', '-{0}yu'.format('S' if refresh else '')]
    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


def install(name=None, refresh=False, fromrepo=None,
            pkgs=None, sources=None, **kwargs):
    '''
    Install the passed package

    name
        The name of the package to be installed.

    refresh
        Whether or not to refresh the package database before installing.

    fromrepo
        Specify a package repository (url) to install from.


    Multiple Package Installation Options:

    pkgs
        A list of packages to install from a software repository. Must be
        passed as a python list.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install pkgs='["foo","bar"]'

    sources
        A list of packages to install. Must be passed as a list of dicts,
        with the keys being package names, and the values being the source URI
        or local path to the package.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install sources='[{"foo": "salt://foo.deb"},{"bar": "salt://bar.deb"}]'

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

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

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.install <package name>
    '''

    # XXX sources is not yet used in this code

    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 {}

    if pkg_type != 'repository':
        log.error('xbps: pkg_type "%s" not supported.', pkg_type)
        return {}

    cmd = ['xbps-install']

    if refresh:
        cmd.append('-S')  # update repo db
    if fromrepo:
        cmd.append('--repository={0}'.format(fromrepo))
    cmd.append('-y')      # assume yes when asked
    cmd.extend(pkg_params)

    old = list_pkgs()
    __salt__['cmd.run'](cmd, output_loglevel='trace')
    __context__.pop('pkg.list_pkgs', None)
    new = list_pkgs()

    _rehash()
    return salt.utils.data.compare_dicts(old, new)


def remove(name=None, pkgs=None, recursive=True, **kwargs):
    '''
    name
        The name of the package to be deleted.

    recursive
        Also remove dependent packages (not required elsewhere).
        Default mode: enabled.

    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.

    Returns a list containing the removed packages.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.remove <package name> [recursive=False]
        salt '*' pkg.remove <package1>,<package2>,<package3> [recursive=False]
        salt '*' pkg.remove pkgs='["foo", "bar"]' [recursive=False]
    '''

    try:
        pkg_params, pkg_type = __salt__['pkg_resource.parse_targets'](
            name, pkgs
        )
    except MinionError as exc:
        raise CommandExecutionError(exc)

    if not pkg_params:
        return {}

    old = list_pkgs()

    # keep only installed packages
    targets = [x for x in pkg_params if x in old]
    if not targets:
        return {}

    cmd = ['xbps-remove', '-y']
    if recursive:
        cmd.append('-R')
    cmd.extend(targets)
    __salt__['cmd.run'](cmd, output_loglevel='trace')
    __context__.pop('pkg.list_pkgs', None)
    new = list_pkgs()

    return salt.utils.data.compare_dicts(old, new)


def list_repos():
    '''
    List all repos known by XBPS

    CLI Example:

    .. code-block:: bash

       salt '*' pkg.list_repos
    '''
    repos = {}
    out = __salt__['cmd.run']('xbps-query -L', output_loglevel='trace')
    for line in out.splitlines():
        repo = {}
        if not line:
            continue
        try:
            nb, url, rsa = line.strip().split(' ', 2)
        except ValueError:
            log.error(
                'Problem parsing xbps-query: '
                'Unexpected formatting in line: "%s"', line
            )
        repo['nbpkg'] = int(nb) if nb.isdigit() else 0
        repo['url'] = url
        repo['rsasigned'] = True if rsa == '(RSA signed)' else False
        repos[repo['url']] = repo
    return repos


def get_repo(repo, **kwargs):
    '''
    Display information about the repo.

    CLI Examples:

    .. code-block:: bash

        salt '*' pkg.get_repo 'repo-url'
    '''
    repos = list_repos()
    if repo in repos:
        return repos[repo]
    return {}


def _locate_repo_files(repo, rewrite=False):
    '''
    Find what file a repo is called in.

    Helper function for add_repo() and del_repo()

    repo
        url of the repo to locate (persistent).

    rewrite
        Whether to remove matching repository settings during this process.

    Returns a list of absolute paths.
    '''

    ret_val = []
    files = []
    conf_dirs = ['/etc/xbps.d/', '/usr/share/xbps.d/']
    name_glob = '*.conf'
    # Matches a line where first printing is "repository" and there is an equals
    # sign before the repo, an optional forwardslash at the end of the repo name,
    # and it's possible for there to be a comment after repository=repo
    regex = re.compile(r'\s*repository\s*=\s*'+repo+r'/?\s*(#.*)?$')

    for cur_dir in conf_dirs:
        files.extend(glob.glob(cur_dir+name_glob))

    for filename in files:
        write_buff = []
        with salt.utils.files.fopen(filename, 'r') as cur_file:
            for line in cur_file:
                if regex.match(salt.utils.stringutils.to_unicode(line)):
                    ret_val.append(filename)
                else:
                    write_buff.append(line)
        if rewrite and filename in ret_val:
            if len(write_buff) > 0:
                with salt.utils.files.fopen(filename, 'w') as rewrite_file:
                    rewrite_file.writelines(write_buff)
            else:  # Prune empty files
                os.remove(filename)

    return ret_val


def add_repo(repo, conffile='/usr/share/xbps.d/15-saltstack.conf'):
    '''
    Add an XBPS repository to the system.

    repo
        url of repo to add (persistent).

    conffile
        path to xbps conf file to add this repo
        default: /usr/share/xbps.d/15-saltstack.conf

    CLI Examples:

    .. code-block:: bash

        salt '*' pkg.add_repo <repo url> [conffile=/path/to/xbps/repo.conf]
    '''

    if len(_locate_repo_files(repo)) == 0:
        try:
            with salt.utils.files.fopen(conffile, 'a+') as conf_file:
                conf_file.write(
                    salt.utils.stringutils.to_str(
                        'repository={0}\n'.format(repo)
                    )
                )
        except IOError:
            return False

    return True


def del_repo(repo):
    '''
    Remove an XBPS repository from the system.

    repo
        url of repo to remove (persistent).

    CLI Examples:

    .. code-block:: bash

        salt '*' pkg.del_repo <repo url>
    '''

    try:
        _locate_repo_files(repo, rewrite=True)
    except IOError:
        return False
    else:
        return True
