r'''
    Install the passed package(s) on the system using winrepo

    Args:

        name (str):
            The name of a single package, or a comma-separated list of packages
            to install. (no spaces after the commas)

        refresh (bool):
            Boolean value representing whether or not to refresh the winrepo db.
            Default ``False``.

        pkgs (list):
            A list of packages to install from a software repository. All
            packages listed under ``pkgs`` will be installed via a single
            command.

            You can specify a version by passing the item as a dict:

            CLI Example:

            .. code-block:: bash

                # will install the latest version of foo and bar
                salt '*' pkg.install pkgs='["foo", "bar"]'

                # will install the latest version of foo and version 1.2.3 of bar
                salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3"}]'

    Kwargs:

        version (str):
            The specific version to install. If omitted, the latest version will
            be installed. Recommend for use when installing a single package.

            If passed with a list of packages in the ``pkgs`` parameter, the
            version will be ignored.

            CLI Example:

             .. code-block:: bash

                # Version is ignored
                salt '*' pkg.install pkgs="['foo', 'bar']" version=1.2.3

            If passed with a comma separated list in the ``name`` parameter, the
            version will apply to all packages in the list.

            CLI Example:

             .. code-block:: bash

                # Version 1.2.3 will apply to packages foo and bar
                salt '*' pkg.install foo,bar version=1.2.3

        extra_install_flags (str):
            Additional install flags that will be appended to the
            ``install_flags`` defined in the software definition file. Only
            applies when single package is passed.

        saltenv (str):
            Salt environment. Default 'base'

        report_reboot_exit_codes (bool):
            If the installer exits with a recognized exit code indicating that
            a reboot is required, the module function

               *win_system.set_reboot_required_witnessed*

            will be called, preserving the knowledge of this event for the
            remainder of the current boot session. For the time being, 3010 is
            the only recognized exit code. The value of this param defaults to
            True.

            .. versionadded:: 2016.11.0

    Returns:
        dict: Return a dict containing the new package names and versions. If
        the package is already installed, an empty dict is returned.

        If the package is installed by ``pkg.install``:

        .. code-block:: cfg

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

    The following example will refresh the winrepo and install a single
    package, 7zip.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.install 7zip refresh=True

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.install 7zip
        salt '*' pkg.install 7zip,filezilla
        salt '*' pkg.install pkgs='["7zip","filezilla"]'

    WinRepo Definition File Examples:

    The following example demonstrates the use of ``cache_file``. This would be
    used if you have multiple installers in the same directory that use the
    same ``install.ini`` file and you don't want to download the additional
    installers.

    .. code-block:: bash

        ntp:
          4.2.8:
            installer: 'salt://win/repo/ntp/ntp-4.2.8-win32-setup.exe'
            full_name: Meinberg NTP Windows Client
            locale: en_US
            reboot: False
            cache_file: 'salt://win/repo/ntp/install.ini'
            install_flags: '/USEFILE=C:\salt\var\cache\salt\minion\files\base\win\repo\ntp\install.ini'
            uninstaller: 'NTP/uninst.exe'

    The following example demonstrates the use of ``cache_dir``. It assumes a
    file named ``install.ini`` resides in the same directory as the installer.

    .. code-block:: bash

        ntp:
          4.2.8:
            installer: 'salt://win/repo/ntp/ntp-4.2.8-win32-setup.exe'
            full_name: Meinberg NTP Windows Client
            locale: en_US
            reboot: False
            cache_dir: True
            install_flags: '/USEFILE=C:\salt\var\cache\salt\minion\files\base\win\repo\ntp\install.ini'
            uninstaller: 'NTP/uninst.exe'

def install(name=None, refresh=False, pkgs=None, **kwargs):
    r'''
    Install the passed package(s) on the system using winrepo

    Args:

        name (str):
            The name of a single package, or a comma-separated list of packages
            to install. (no spaces after the commas)

        refresh (bool):
            Boolean value representing whether or not to refresh the winrepo db.
            Default ``False``.

        pkgs (list):
            A list of packages to install from a software repository. All
            packages listed under ``pkgs`` will be installed via a single
            command.

            You can specify a version by passing the item as a dict:

            CLI Example:

            .. code-block:: bash

                # will install the latest version of foo and bar
                salt '*' pkg.install pkgs='["foo", "bar"]'

                # will install the latest version of foo and version 1.2.3 of bar
                salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3"}]'

    Kwargs:

        version (str):
            The specific version to install. If omitted, the latest version will
            be installed. Recommend for use when installing a single package.

            If passed with a list of packages in the ``pkgs`` parameter, the
            version will be ignored.

            CLI Example:

             .. code-block:: bash

                # Version is ignored
                salt '*' pkg.install pkgs="['foo', 'bar']" version=1.2.3

            If passed with a comma separated list in the ``name`` parameter, the
            version will apply to all packages in the list.

            CLI Example:

             .. code-block:: bash

                # Version 1.2.3 will apply to packages foo and bar
                salt '*' pkg.install foo,bar version=1.2.3

        extra_install_flags (str):
            Additional install flags that will be appended to the
            ``install_flags`` defined in the software definition file. Only
            applies when single package is passed.

        saltenv (str):
            Salt environment. Default 'base'

        report_reboot_exit_codes (bool):
            If the installer exits with a recognized exit code indicating that
            a reboot is required, the module function

               *win_system.set_reboot_required_witnessed*

            will be called, preserving the knowledge of this event for the
            remainder of the current boot session. For the time being, 3010 is
            the only recognized exit code. The value of this param defaults to
            True.

            .. versionadded:: 2016.11.0

    Returns:
        dict: Return a dict containing the new package names and versions. If
        the package is already installed, an empty dict is returned.

        If the package is installed by ``pkg.install``:

        .. code-block:: cfg

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

    The following example will refresh the winrepo and install a single
    package, 7zip.

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.install 7zip refresh=True

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.install 7zip
        salt '*' pkg.install 7zip,filezilla
        salt '*' pkg.install pkgs='["7zip","filezilla"]'

    WinRepo Definition File Examples:

    The following example demonstrates the use of ``cache_file``. This would be
    used if you have multiple installers in the same directory that use the
    same ``install.ini`` file and you don't want to download the additional
    installers.

    .. code-block:: bash

        ntp:
          4.2.8:
            installer: 'salt://win/repo/ntp/ntp-4.2.8-win32-setup.exe'
            full_name: Meinberg NTP Windows Client
            locale: en_US
            reboot: False
            cache_file: 'salt://win/repo/ntp/install.ini'
            install_flags: '/USEFILE=C:\salt\var\cache\salt\minion\files\base\win\repo\ntp\install.ini'
            uninstaller: 'NTP/uninst.exe'

    The following example demonstrates the use of ``cache_dir``. It assumes a
    file named ``install.ini`` resides in the same directory as the installer.

    .. code-block:: bash

        ntp:
          4.2.8:
            installer: 'salt://win/repo/ntp/ntp-4.2.8-win32-setup.exe'
            full_name: Meinberg NTP Windows Client
            locale: en_US
            reboot: False
            cache_dir: True
            install_flags: '/USEFILE=C:\salt\var\cache\salt\minion\files\base\win\repo\ntp\install.ini'
            uninstaller: 'NTP/uninst.exe'
    '''
    ret = {}
    saltenv = kwargs.pop('saltenv', 'base')

    refresh = salt.utils.data.is_true(refresh)
    # no need to call _refresh_db_conditional as list_pkgs will do it

    # Make sure name or pkgs is passed
    if not name and not pkgs:
        return 'Must pass a single package or a list of packages'

    # Ignore pkg_type from parse_targets, Windows does not support the
    # "sources" argument
    pkg_params = __salt__['pkg_resource.parse_targets'](name, pkgs, **kwargs)[0]

    if len(pkg_params) > 1:
        if kwargs.get('extra_install_flags') is not None:
            log.warning('\'extra_install_flags\' argument will be ignored for '
                        'multiple package targets')

    # Windows expects an Options dictionary containing 'version'
    for pkg in pkg_params:
        pkg_params[pkg] = {'version': pkg_params[pkg]}

    if not pkg_params:
        log.error('No package definition found')
        return {}

    if not pkgs and len(pkg_params) == 1:
        # Only use the 'version' param if a single item was passed to the 'name'
        # parameter
        pkg_params = {
            name: {
                'version': kwargs.get('version'),
                'extra_install_flags': kwargs.get('extra_install_flags')
            }
        }

    # Get a list of currently installed software for comparison at the end
    old = list_pkgs(saltenv=saltenv, refresh=refresh, versions_as_list=True)

    # Loop through each package
    changed = []
    for pkg_name, options in six.iteritems(pkg_params):

        # Load package information for the package
        pkginfo = _get_package_info(pkg_name, saltenv=saltenv)

        # Make sure pkginfo was found
        if not pkginfo:
            log.error('Unable to locate package %s', pkg_name)
            ret[pkg_name] = 'Unable to locate package {0}'.format(pkg_name)
            continue

        version_num = options.get('version')
        #  Using the salt cmdline with version=5.3 might be interpreted
        #  as a float it must be converted to a string in order for
        #  string matching to work.
        if not isinstance(version_num, six.string_types) and version_num is not None:
            version_num = six.text_type(version_num)

        # If the version was not passed, version_num will be None
        if not version_num:
            if pkg_name in old:
                log.debug('pkg.install: \'%s\' version \'%s\' is already installed',
                          pkg_name, old[pkg_name][0])
                continue
            # Get the most recent version number available from winrepo.p
            # May also return `latest` or an empty string
            version_num = _get_latest_pkg_version(pkginfo)

        if version_num == 'latest' and 'latest' not in pkginfo:
            # Get the most recent version number available from winrepo.p
            # May also return `latest` or an empty string
            version_num = _get_latest_pkg_version(pkginfo)

        # Check if the version is already installed
        if version_num in old.get(pkg_name, []):
            # Desired version number already installed
            log.debug('pkg.install: \'%s\' version \'%s\' is already installed',
                      pkg_name, version_num)
            continue
        # If version number not installed, is the version available?
        elif version_num != 'latest' and version_num not in pkginfo:
            log.error('Version %s not found for package %s',
                      version_num, pkg_name)
            ret[pkg_name] = {'not found': version_num}
            continue

        # Get the installer settings from winrepo.p
        installer = pkginfo[version_num].get('installer', '')
        cache_dir = pkginfo[version_num].get('cache_dir', False)
        cache_file = pkginfo[version_num].get('cache_file', '')

        # Is there an installer configured?
        if not installer:
            log.error('No installer configured for version %s of package %s',
                      version_num, pkg_name)
            ret[pkg_name] = {'no installer': version_num}
            continue

        # Is the installer in a location that requires caching
        if installer.startswith(('salt:', 'http:', 'https:', 'ftp:')):

            # Check for the 'cache_dir' parameter in the .sls file
            # If true, the entire directory will be cached instead of the
            # individual file. This is useful for installations that are not
            # single files
            if cache_dir and installer.startswith('salt:'):
                path, _ = os.path.split(installer)
                __salt__['cp.cache_dir'](path=path,
                                         saltenv=saltenv,
                                         include_empty=False,
                                         include_pat=None,
                                         exclude_pat='E@init.sls$')

            # Check to see if the cache_file is cached... if passed
            if cache_file and cache_file.startswith('salt:'):

                # Check to see if the file is cached
                cached_file = __salt__['cp.is_cached'](cache_file, saltenv)
                if not cached_file:
                    cached_file = __salt__['cp.cache_file'](cache_file, saltenv)

                # Make sure the cached file is the same as the source
                if __salt__['cp.hash_file'](cache_file, saltenv) != \
                        __salt__['cp.hash_file'](cached_file):
                    cached_file = __salt__['cp.cache_file'](cache_file, saltenv)

                    # Check if the cache_file was cached successfully
                    if not cached_file:
                        log.error('Unable to cache %s', cache_file)
                        ret[pkg_name] = {
                            'failed to cache cache_file': cache_file
                        }
                        continue

            # Check to see if the installer is cached
            cached_pkg = __salt__['cp.is_cached'](installer, saltenv)
            if not cached_pkg:
                # It's not cached. Cache it, mate.
                cached_pkg = __salt__['cp.cache_file'](installer, saltenv)

                # Check if the installer was cached successfully
                if not cached_pkg:
                    log.error(
                        'Unable to cache file %s from saltenv: %s',
                        installer, saltenv
                    )
                    ret[pkg_name] = {'unable to cache': installer}
                    continue

            # Compare the hash of the cached installer to the source only if the
            # file is hosted on salt:
            if installer.startswith('salt:'):
                if __salt__['cp.hash_file'](installer, saltenv) != \
                        __salt__['cp.hash_file'](cached_pkg):
                    try:
                        cached_pkg = __salt__['cp.cache_file'](installer, saltenv)
                    except MinionError as exc:
                        return '{0}: {1}'.format(exc, installer)

                    # Check if the installer was cached successfully
                    if not cached_pkg:
                        log.error('Unable to cache %s', installer)
                        ret[pkg_name] = {'unable to cache': installer}
                        continue
        else:
            # Run the installer directly (not hosted on salt:, https:, etc.)
            cached_pkg = installer

        # Fix non-windows slashes
        cached_pkg = cached_pkg.replace('/', '\\')
        cache_path = os.path.dirname(cached_pkg)

        # Compare the hash sums
        source_hash = pkginfo[version_num].get('source_hash', False)
        if source_hash:
            source_sum = _get_source_sum(source_hash, cached_pkg, saltenv)
            log.debug('pkg.install: Source %s hash: %s',
                      source_sum['hash_type'], source_sum['hsum'])

            cached_pkg_sum = salt.utils.hashutils.get_hash(cached_pkg,
                                                           source_sum['hash_type'])
            log.debug('pkg.install: Package %s hash: %s',
                      source_sum['hash_type'], cached_pkg_sum)

            if source_sum['hsum'] != cached_pkg_sum:
                raise SaltInvocationError(
                    ("Source hash '{0}' does not match package hash"
                     " '{1}'").format(source_sum['hsum'], cached_pkg_sum)
                )
            log.debug('pkg.install: Source hash matches package hash.')

        # Get install flags

        install_flags = pkginfo[version_num].get('install_flags', '')
        if options and options.get('extra_install_flags'):
            install_flags = '{0} {1}'.format(
                install_flags,
                options.get('extra_install_flags', '')
            )

        # Compute msiexec string
        use_msiexec, msiexec = _get_msiexec(pkginfo[version_num].get('msiexec', False))

        # Build cmd and arguments
        # cmd and arguments must be separated for use with the task scheduler
        cmd_shell = os.getenv('ComSpec', '{0}\\system32\\cmd.exe'.format(os.getenv('WINDIR')))
        if use_msiexec:
            arguments = '"{0}" /I "{1}"'.format(msiexec, cached_pkg)
            if pkginfo[version_num].get('allusers', True):
                arguments = '{0} ALLUSERS=1'.format(arguments)
        else:
            arguments = '"{0}"'.format(cached_pkg)

        if install_flags:
            arguments = '{0} {1}'.format(arguments, install_flags)

        # Install the software
        # Check Use Scheduler Option
        if pkginfo[version_num].get('use_scheduler', False):
            # Create Scheduled Task
            __salt__['task.create_task'](name='update-salt-software',
                                         user_name='System',
                                         force=True,
                                         action_type='Execute',
                                         cmd=cmd_shell,
                                         arguments='/s /c "{0}"'.format(arguments),
                                         start_in=cache_path,
                                         trigger_type='Once',
                                         start_date='1975-01-01',
                                         start_time='01:00',
                                         ac_only=False,
                                         stop_if_on_batteries=False)

            # Run Scheduled Task
            # Special handling for installing salt
            if re.search(r'salt[\s_.-]*minion',
                         pkg_name,
                         flags=re.IGNORECASE + re.UNICODE) is not None:
                ret[pkg_name] = {'install status': 'task started'}
                if not __salt__['task.run'](name='update-salt-software'):
                    log.error('Failed to install %s', pkg_name)
                    log.error('Scheduled Task failed to run')
                    ret[pkg_name] = {'install status': 'failed'}
                else:

                    # Make sure the task is running, try for 5 secs
                    t_end = time.time() + 5
                    while time.time() < t_end:
                        time.sleep(0.25)
                        task_running = __salt__['task.status'](
                                'update-salt-software') == 'Running'
                        if task_running:
                            break

                    if not task_running:
                        log.error('Failed to install %s', pkg_name)
                        log.error('Scheduled Task failed to run')
                        ret[pkg_name] = {'install status': 'failed'}

            # All other packages run with task scheduler
            else:
                if not __salt__['task.run_wait'](name='update-salt-software'):
                    log.error('Failed to install %s', pkg_name)
                    log.error('Scheduled Task failed to run')
                    ret[pkg_name] = {'install status': 'failed'}
        else:
            # Launch the command
            result = __salt__['cmd.run_all']('"{0}" /s /c "{1}"'.format(cmd_shell, arguments),
                                             cache_path,
                                             output_loglevel='trace',
                                             python_shell=False,
                                             redirect_stderr=True)
            if not result['retcode']:
                ret[pkg_name] = {'install status': 'success'}
                changed.append(pkg_name)
            elif result['retcode'] == 3010:
                # 3010 is ERROR_SUCCESS_REBOOT_REQUIRED
                report_reboot_exit_codes = kwargs.pop(
                    'report_reboot_exit_codes', True)
                if report_reboot_exit_codes:
                    __salt__['system.set_reboot_required_witnessed']()
                ret[pkg_name] = {'install status': 'success, reboot required'}
                changed.append(pkg_name)
            elif result['retcode'] == 1641:
                # 1641 is ERROR_SUCCESS_REBOOT_INITIATED
                ret[pkg_name] = {'install status': 'success, reboot initiated'}
                changed.append(pkg_name)
            else:
                log.error('Failed to install %s', pkg_name)
                log.error('retcode %s', result['retcode'])
                log.error('installer output: %s', result['stdout'])
                ret[pkg_name] = {'install status': 'failed'}

    # Get a new list of installed software
    new = list_pkgs(saltenv=saltenv, refresh=False)

    # Take the "old" package list and convert the values to strings in
    # preparation for the comparison below.
    __salt__['pkg_resource.stringify'](old)

    # Check for changes in the registry
    difference = salt.utils.data.compare_dicts(old, new)

    # Compare the software list before and after
    # Add the difference to ret
    ret.update(difference)

    return ret