"""
Runner to manage Windows software repo
"""

# WARNING: Any modules imported here must also be added to
# salt/modules/win_repo.py


import logging
import os

import salt.loader
import salt.minion
import salt.template
import salt.utils.files
import salt.utils.gitfs
import salt.utils.msgpack
import salt.utils.path
from salt.exceptions import CommandExecutionError, SaltRenderError

log = logging.getLogger(__name__)

# Global parameters which can be overridden on a per-remote basis
PER_REMOTE_OVERRIDES = ("ssl_verify", "refspecs", "fallback")

# Fall back to default per-remote-only. This isn't technically needed since
# salt.utils.gitfs.GitBase.__init__ will default to
# salt.utils.gitfs.PER_REMOTE_ONLY for this value, so this is mainly for
# runners and other modules that import salt.runners.winrepo.
PER_REMOTE_ONLY = salt.utils.gitfs.PER_REMOTE_ONLY
GLOBAL_ONLY = ("branch",)


def _legacy_git():
    return not any(
        (salt.utils.gitfs.GITPYTHON_VERSION, salt.utils.gitfs.PYGIT2_VERSION)
    )


def genrepo(opts=None, fire_event=True):
    """
    Generate winrepo_cachefile based on sls files in the winrepo_dir

    opts
        Specify an alternate opts dict. Should not be used unless this function
        is imported into an execution module.

    fire_event : True
        Fire an event on failure. Only supported on the master.

    CLI Example:

    .. code-block:: bash

        salt-run winrepo.genrepo
    """
    if opts is None:
        opts = __opts__

    winrepo_dir = opts["winrepo_dir"]
    winrepo_cachefile = opts["winrepo_cachefile"]

    ret = {}
    if not os.path.exists(winrepo_dir):
        os.makedirs(winrepo_dir)
    renderers = salt.loader.render(opts, __salt__)
    for root, _, files in salt.utils.path.os_walk(winrepo_dir):
        for name in files:
            if name.endswith(".sls"):
                try:
                    config = salt.template.compile_template(
                        os.path.join(root, name),
                        renderers,
                        opts["renderer"],
                        opts["renderer_blacklist"],
                        opts["renderer_whitelist"],
                    )
                except SaltRenderError as exc:
                    log.debug("Failed to render %s.", os.path.join(root, name))
                    log.debug("Error: %s.", exc)
                    continue
                if config:
                    revmap = {}
                    for pkgname, versions in config.items():
                        log.debug("Compiling winrepo data for package '%s'", pkgname)
                        for version, repodata in versions.items():
                            log.debug(
                                "Compiling winrepo data for %s version %s",
                                pkgname,
                                version,
                            )
                            if not isinstance(version, str):
                                config[pkgname][str(version)] = config[pkgname].pop(
                                    version
                                )
                            if not isinstance(repodata, dict):
                                msg = "Failed to compile {}.".format(
                                    os.path.join(root, name)
                                )
                                log.debug(msg)
                                if fire_event:
                                    try:
                                        __jid_event__.fire_event(
                                            {"error": msg}, "progress"
                                        )
                                    except NameError:
                                        log.error(
                                            "Attempted to fire the an event "
                                            "with the following error, but "
                                            "event firing is not supported: %s",
                                            msg,
                                        )
                                continue
                            revmap[repodata["full_name"]] = pkgname
                    ret.setdefault("repo", {}).update(config)
                    ret.setdefault("name_map", {}).update(revmap)
    with salt.utils.files.fopen(
        os.path.join(winrepo_dir, winrepo_cachefile), "w+b"
    ) as repo:
        repo.write(salt.utils.msgpack.dumps(ret))
    return ret


def update_git_repos(opts=None, clean=False, masterless=False):
    """
    Checkout git repos containing Windows Software Package Definitions

    opts
        Specify an alternate opts dict. Should not be used unless this function
        is imported into an execution module.

    clean : False
        Clean repo cachedirs which are not configured under
        :conf_master:`winrepo_remotes`.

        .. warning::
            This argument should not be set to ``True`` if a mix of git and
            non-git repo definitions are being used, as it will result in the
            non-git repo definitions being removed.

        .. versionadded:: 2015.8.0

    CLI Examples:

    .. code-block:: bash

        salt-run winrepo.update_git_repos
        salt-run winrepo.update_git_repos clean=True
    """
    if opts is None:
        opts = __opts__

    winrepo_dir = opts["winrepo_dir"]
    winrepo_remotes = opts["winrepo_remotes"]

    winrepo_cfg = [
        (winrepo_remotes, winrepo_dir),
        (opts["winrepo_remotes_ng"], opts["winrepo_dir_ng"]),
    ]

    ret = {}
    for remotes, base_dir in winrepo_cfg:
        if _legacy_git():
            # Use legacy code
            winrepo_result = {}
            for remote_info in remotes:
                if "/" in remote_info:
                    targetname = remote_info.split("/")[-1]
                else:
                    targetname = remote_info
                rev = "HEAD"
                # If a revision is specified, use it.
                try:
                    rev, remote_url = remote_info.strip().split()
                except ValueError:
                    remote_url = remote_info
                targetname = targetname.replace(".git", "")
                # GitFS using pygit2 and gitpython place the repo in a
                # subdirectory named `_`. We need to stay consistent when using
                # the legacy method as well
                gittarget = os.path.join(base_dir, targetname, "_")
                if masterless:
                    result = __salt__["state.single"](
                        "git.latest",
                        name=remote_url,
                        rev=rev,
                        branch="winrepo",
                        target=gittarget,
                        force_checkout=True,
                        force_reset=True,
                    )
                    if isinstance(result, list):
                        # Errors were detected
                        raise CommandExecutionError(
                            "Failed up update winrepo remotes: {}".format(
                                "\n".join(result)
                            )
                        )
                    if "name" not in result:
                        # Highstate output dict, the results are actually nested
                        # one level down.
                        key = next(iter(result))
                        result = result[key]
                else:
                    mminion = salt.minion.MasterMinion(opts)
                    result = mminion.functions["state.single"](
                        "git.latest",
                        name=remote_url,
                        rev=rev,
                        branch="winrepo",
                        target=gittarget,
                        force_checkout=True,
                        force_reset=True,
                    )
                    if isinstance(result, list):
                        # Errors were detected
                        raise CommandExecutionError(
                            "Failed to update winrepo remotes: {}".format(
                                "\n".join(result)
                            )
                        )
                    if "name" not in result:
                        # Highstate output dict, the results are actually nested
                        # one level down.
                        key = next(iter(result))
                        result = result[key]
                winrepo_result[result["name"]] = result["result"]
            ret.update(winrepo_result)
        else:
            # New winrepo code utilizing salt.utils.gitfs
            try:
                winrepo = salt.utils.gitfs.WinRepo(
                    opts,
                    remotes,
                    per_remote_overrides=PER_REMOTE_OVERRIDES,
                    per_remote_only=PER_REMOTE_ONLY,
                    global_only=GLOBAL_ONLY,
                    cache_root=base_dir,
                )
                winrepo.fetch_remotes()
                # Since we're not running update(), we need to manually call
                # clear_old_remotes() to remove directories from remotes that
                # have been removed from configuration.
                if clean:
                    winrepo.clear_old_remotes()
                winrepo.checkout()
            except Exception as exc:  # pylint: disable=broad-except
                msg = f"Failed to update winrepo_remotes: {exc}"
                log.error(msg, exc_info_on_loglevel=logging.DEBUG)
                return msg
            ret.update(winrepo.winrepo_dirs)
    return ret
