"""
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
        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
        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
