"""
Create ssh executor system
"""

import logging
import os
import shutil
import tarfile
import tempfile
from contextlib import closing

import salt.client.ssh
import salt.client.ssh.shell
import salt.loader
import salt.minion
import salt.roster
import salt.state
import salt.utils.files
import salt.utils.json
import salt.utils.path
import salt.utils.stringutils
import salt.utils.thin
import salt.utils.url
import salt.utils.verify

log = logging.getLogger(__name__)


class SSHState(salt.state.State):
    """
    Create a State object which wraps the SSH functions for state operations
    """

    def __init__(
        self,
        opts,
        pillar_override=None,
        wrapper=None,
        context=None,
        initial_pillar=None,
    ):
        self.wrapper = wrapper
        self.context = context
        # ``opts`` is the per-minion opts package returned by
        # ``test.opts_pkg`` running inside salt-thin on the target. Its
        # ``cachedir`` is rooted under the on-target ``thin_dir`` (e.g.
        # ``/var/tmp/.root_XXXXX_salt/running_data/var/cache/salt``).
        # The state runs on the master, so the state fileclient and the
        # jinja loader search path
        # (``opts['cachedir']/files/<saltenv>``) need to be anchored
        # under the configured master ``cachedir`` instead. See #68458.
        if wrapper is not None and getattr(wrapper, "fsclient", None) is not None:
            opts["cachedir"] = wrapper.fsclient.opts["cachedir"]
        super().__init__(opts, pillar_override, initial_pillar=initial_pillar)

    def load_modules(self, data=None, proxy=None):
        """
        Load up the modules for remote compilation via ssh
        """
        self.functions = self.wrapper
        self.utils = salt.loader.utils(self.opts)
        self.serializers = salt.loader.serializers(self.opts)
        locals_ = salt.loader.minion_mods(self.opts, utils=self.utils)
        self.states = salt.loader.states(
            self.opts, locals_, self.utils, self.serializers
        )
        self.rend = salt.loader.render(self.opts, self.functions)

    def _gather_pillar(self):
        """
        The opts used during pillar rendering should contain the master
        opts in the root namespace. self.opts is the modified minion opts,
        containing the original master opts in __master_opts__.
        """
        _opts = self.opts
        popts = {}
        # Pillar compilation needs the master opts primarily,
        # same as during regular operation.
        popts.update(_opts)
        popts.update(_opts.get("__master_opts__", {}))
        # But, salt.state.State takes the parameters for get_pillar from
        # the opts, so we need to ensure they are correct for the minion.
        popts["id"] = _opts["id"]
        popts["saltenv"] = _opts["saltenv"]
        popts["pillarenv"] = _opts.get("pillarenv")
        self.opts = popts
        pillar = super()._gather_pillar()
        self.opts = _opts
        return pillar

    def check_refresh(self, data, ret):
        """
        Stub out check_refresh
        """
        return

    def module_refresh(self):
        """
        Module refresh is not needed, stub it out
        """
        return


class SSHHighState(salt.state.BaseHighState):
    """
    Used to compile the highstate on the master
    """

    stack = []

    def __init__(
        self,
        opts,
        pillar_override=None,
        wrapper=None,
        fsclient=None,
        context=None,
        initial_pillar=None,
    ):
        self.client = fsclient
        # ``opts`` is the per-minion opts package; its ``cachedir`` is a
        # thin_dir-relative path on the target. The highstate runs on the
        # master, so anchor ``opts['cachedir']`` under the master
        # ``cachedir`` (taken from the master-side fileclient) so master
        # fileserver caching and jinja template resolution don't write to
        # the minion's thin_dir path on the master filesystem. See
        # #68458.
        if fsclient is not None and getattr(fsclient, "opts", None) is not None:
            opts["cachedir"] = fsclient.opts["cachedir"]
        salt.state.BaseHighState.__init__(self, opts)
        self.state = SSHState(
            opts,
            pillar_override,
            wrapper,
            context=context,
            initial_pillar=initial_pillar,
        )
        self.matchers = salt.loader.matchers(self.opts)
        self.tops = salt.loader.tops(self.opts)

        self._pydsl_all_decls = {}
        self._pydsl_render_stack = []

    def push_active(self):
        salt.state.HighState.stack.append(self)

    def load_dynamic(self, matches):
        """
        Stub out load_dynamic
        """
        return

    def _master_tops(self):
        """
        Evaluate master_tops locally
        """
        if "id" not in self.opts:
            log.error("Received call for external nodes without an id")
            return {}
        if not salt.utils.verify.valid_id(self.opts, self.opts["id"]):
            return {}
        # Evaluate all configured master_tops interfaces

        grains = {}
        ret = {}

        if "grains" in self.opts:
            grains = self.opts["grains"]
        for fun in self.tops:
            if fun not in self.opts.get("master_tops", {}):
                continue
            try:
                ret.update(self.tops[fun](opts=self.opts, grains=grains))
            except Exception as exc:  # pylint: disable=broad-except
                # If anything happens in the top generation, log it and move on
                log.error(
                    "Top function %s failed with error %s for minion %s",
                    fun,
                    exc,
                    self.opts["id"],
                )
        return ret

    def destroy(self):
        if self.client:
            self.client.destroy()

    def __enter__(self):
        return self

    def __exit__(self, *_):
        self.destroy()


def lowstate_file_refs(chunks, extras=""):
    """
    Create a list of file ref objects to reconcile
    """
    refs = {}
    for chunk in chunks:
        if not isinstance(chunk, dict):
            continue
        saltenv = "base"
        crefs = []
        for state in chunk:
            if state == "__env__":
                saltenv = chunk[state]
            elif state.startswith("__"):
                continue
            crefs.extend(salt_refs(chunk[state]))
        if saltenv not in refs:
            refs[saltenv] = []
        if crefs:
            refs[saltenv].append(crefs)
    if extras:
        extra_refs = extras.split(",")
        if extra_refs:
            for env in refs:
                for x in extra_refs:
                    refs[env].append([x])
    return refs


def salt_refs(data, ret=None):
    """
    Pull salt file references out of the states
    """
    proto = "salt://"
    if ret is None:
        ret = []
    if isinstance(data, str):
        if data.startswith(proto) and data not in ret:
            ret.append(data)
    if isinstance(data, list):
        for comp in data:
            salt_refs(comp, ret)
    if isinstance(data, dict):
        for comp in data:
            salt_refs(data[comp], ret)
    return ret


def prep_trans_tar(
    file_client, chunks, file_refs, pillar=None, id_=None, roster_grains=None
):
    """
    Generate the execution package from the saltenv file refs and a low state
    data structure
    """
    gendir = tempfile.mkdtemp()
    trans_tar = salt.utils.files.mkstemp()
    lowfn = os.path.join(gendir, "lowstate.json")
    pillarfn = os.path.join(gendir, "pillar.json")
    roster_grainsfn = os.path.join(gendir, "roster_grains.json")
    sync_refs = [
        [salt.utils.url.create("_modules")],
        [salt.utils.url.create("_states")],
        [salt.utils.url.create("_grains")],
        [salt.utils.url.create("_renderers")],
        [salt.utils.url.create("_returners")],
        [salt.utils.url.create("_output")],
        [salt.utils.url.create("_utils")],
    ]
    with salt.utils.files.fopen(lowfn, "w+") as fp_:
        salt.utils.json.dump(chunks, fp_)
    if pillar:
        with salt.utils.files.fopen(pillarfn, "w+") as fp_:
            salt.utils.json.dump(pillar, fp_)
    if roster_grains:
        with salt.utils.files.fopen(roster_grainsfn, "w+") as fp_:
            salt.utils.json.dump(roster_grains, fp_)

    if id_ is None:
        id_ = ""
    try:
        cachedir = os.path.join("salt-ssh", id_).rstrip(os.sep)
    except AttributeError:
        # Minion ID should always be a str, but don't let an int break this
        cachedir = os.path.join("salt-ssh", str(id_)).rstrip(os.sep)

    for saltenv in file_refs:
        # Location where files in this saltenv will be cached
        cache_dest_root = os.path.join(cachedir, "files", saltenv)
        file_refs[saltenv].extend(sync_refs)
        env_root = os.path.join(gendir, saltenv)
        if not os.path.isdir(env_root):
            os.makedirs(env_root)
        for ref in file_refs[saltenv]:
            for name in ref:
                short = salt.utils.url.parse(name)[0].lstrip("/")
                cache_dest = os.path.join(cache_dest_root, short)
                try:
                    path = file_client.cache_file(name, saltenv, cachedir=cachedir)
                except OSError:
                    path = ""
                if path:
                    tgt = os.path.join(env_root, short)
                    tgt_dir = os.path.dirname(tgt)
                    if not os.path.isdir(tgt_dir):
                        os.makedirs(tgt_dir)
                    shutil.copy(path, tgt)
                    continue
                try:
                    files = file_client.cache_dir(name, saltenv, cachedir=cachedir)
                except OSError:
                    files = ""
                if files:
                    for filename in files:
                        fn = filename[
                            len(file_client.get_cachedir(cache_dest)) :
                        ].strip("/")
                        tgt = os.path.join(env_root, short, fn)
                        tgt_dir = os.path.dirname(tgt)
                        if not os.path.isdir(tgt_dir):
                            os.makedirs(tgt_dir)
                        shutil.copy(filename, tgt)
                    continue
    try:
        # cwd may not exist if it was removed but salt was run from it
        cwd = os.getcwd()
    except OSError:
        cwd = None
    os.chdir(gendir)
    with closing(tarfile.open(trans_tar, "w:gz")) as tfp:
        for root, dirs, files in salt.utils.path.os_walk(gendir):
            for name in files:
                full = os.path.join(root, name)
                tfp.add(full[len(gendir) :].lstrip(os.sep))
    if cwd:
        os.chdir(cwd)
    shutil.rmtree(gendir)
    return trans_tar
