"""
This package contains the loader modules for the salt streams system
"""

import copy
import logging
import re
import sys

import salt.utils.event
import salt.utils.minion

log = logging.getLogger(__name__)


class Beacon:
    """
    This class is used to evaluate and execute on the beacon system
    """

    def __init__(self, opts, functions, interval_map=None):
        import salt.loader

        self.opts = opts
        self.functions = functions
        self.beacons = salt.loader.beacons(opts, functions)
        self.interval_map = interval_map or dict()

    def _close_beacon(self, name):
        """
        Close a single beacon module if it has a close function.
        This releases resources like inotify file descriptors.
        """
        beacon_config = self.opts["beacons"].get(name)
        if beacon_config is None:
            return

        current_beacon_config = None
        if isinstance(beacon_config, list):
            current_beacon_config = {}
            list(map(current_beacon_config.update, beacon_config))
        elif isinstance(beacon_config, dict):
            current_beacon_config = beacon_config

        if current_beacon_config is None:
            return

        beacon_name = name
        if self._determine_beacon_config(current_beacon_config, "beacon_module"):
            beacon_name = current_beacon_config["beacon_module"]

        close_str = f"{beacon_name}.close"
        if close_str in self.beacons:
            try:
                config = copy.deepcopy(beacon_config)
                if isinstance(config, list):
                    config.append({"_beacon_name": name})
                log.debug("Closing beacon %s", name)
                self.beacons[close_str](config)
            except Exception:  # pylint: disable=broad-except
                log.debug("Failed to close beacon %s", name, exc_info=True)

    def close_beacons(self):
        """
        Close all beacon modules that have a close function.
        This ensures resources like inotify file descriptors are properly
        released when beacons are refreshed or the Beacon instance is replaced.

        See: https://github.com/saltstack/salt/issues/66449
        See: https://github.com/saltstack/salt/issues/58907
        """
        beacons = self._get_beacons()
        for mod in beacons:
            if mod == "enabled":
                continue
            self._close_beacon(mod)

    def process(self, config, grains):
        """
        Process the configured beacons

        The config must be a list and looks like this in yaml

        .. code_block:: yaml
            beacons:
              inotify:
                - files:
                    - /etc/fstab: {}
                    - /var/cache/foo: {}
        """
        ret = []
        b_config = copy.deepcopy(config)
        if "enabled" in b_config and not b_config["enabled"]:
            return
        for mod in config:
            if mod == "enabled":
                continue

            # Convert beacons that are lists to a dict to make processing easier
            current_beacon_config = None
            if isinstance(config[mod], list):
                current_beacon_config = {}
                list(map(current_beacon_config.update, config[mod]))
            elif isinstance(config[mod], dict):
                current_beacon_config = config[mod]

            if "enabled" in current_beacon_config:
                if not current_beacon_config["enabled"]:
                    log.trace("Beacon %s disabled", mod)
                    continue
                else:
                    # remove 'enabled' item before processing the beacon
                    if isinstance(config[mod], dict):
                        del config[mod]["enabled"]
                    else:
                        self._remove_list_item(config[mod], "enabled")

            log.trace("Beacon processing: %s", mod)
            beacon_name = None
            if self._determine_beacon_config(current_beacon_config, "beacon_module"):
                beacon_name = current_beacon_config["beacon_module"]
            else:
                beacon_name = mod

            # Run the validate function if it's available,
            # otherwise there is a warning about it being missing
            validate_str = f"{beacon_name}.validate"
            if validate_str in self.beacons:
                valid, vcomment = self.beacons[validate_str](b_config[mod])

                if not valid:
                    log.error(
                        "Beacon %s configuration invalid, not running.\n%s",
                        mod,
                        vcomment,
                    )
                    continue
            else:
                log.warning(
                    "No validate function found for %s, running basic beacon validation.",
                    mod,
                )
                if not isinstance(b_config[mod], list):
                    log.error("Configuration for beacon must be a list.")
                    continue

            b_config[mod].append({"_beacon_name": mod})
            fun_str = f"{beacon_name}.beacon"
            if fun_str in self.beacons:
                runonce = self._determine_beacon_config(
                    current_beacon_config, "run_once"
                )
                interval = self._determine_beacon_config(
                    current_beacon_config, "interval"
                )
                if interval:
                    b_config = self._trim_config(b_config, mod, "interval")
                    if not self._process_interval(mod, interval):
                        log.trace("Skipping beacon %s. Interval not reached.", mod)
                        continue
                if self._determine_beacon_config(
                    current_beacon_config, "disable_during_state_run"
                ):
                    log.trace(
                        "Evaluting if beacon %s should be skipped due to a state run.",
                        mod,
                    )
                    b_config = self._trim_config(
                        b_config, mod, "disable_during_state_run"
                    )
                    is_running = False
                    running_jobs = salt.utils.minion.running(self.opts)
                    for job in running_jobs:
                        if re.match("state.*", job["fun"]):
                            is_running = True
                    if is_running:
                        close_str = f"{beacon_name}.close"
                        if close_str in self.beacons:
                            log.info("Closing beacon %s. State run in progress.", mod)
                            self.beacons[close_str](b_config[mod])
                        else:
                            log.info("Skipping beacon %s. State run in progress.", mod)
                        continue
                # Update __grains__ on the beacon
                self.beacons[fun_str].__globals__["__grains__"] = grains

                # Fire the beacon!
                error = None
                try:
                    raw = self.beacons[fun_str](b_config[mod])
                except:  # pylint: disable=bare-except
                    error = f"{sys.exc_info()[1]}"
                    log.error("Unable to start %s beacon, %s", mod, error)
                    # send beacon error event
                    tag = "salt/beacon/{}/{}/".format(self.opts["id"], mod)
                    ret.append(
                        {
                            "tag": tag,
                            "error": error,
                            "data": {},
                            "beacon_name": beacon_name,
                        }
                    )
                if not error:
                    for data in raw:
                        tag = "salt/beacon/{}/{}/".format(self.opts["id"], mod)
                        if "tag" in data:
                            tag += data.pop("tag")
                        if "id" not in data:
                            data["id"] = self.opts["id"]
                        ret.append(
                            {"tag": tag, "data": data, "beacon_name": beacon_name}
                        )
                    if runonce:
                        self.disable_beacon(mod)
            else:
                log.warning("Unable to process beacon %s", mod)
        return ret

    def _trim_config(self, b_config, mod, key):
        """
        Take a beacon configuration and strip out the interval bits
        """
        if isinstance(b_config[mod], list):
            self._remove_list_item(b_config[mod], key)
        elif isinstance(b_config[mod], dict):
            b_config[mod].pop(key)
        return b_config

    def _determine_beacon_config(self, current_beacon_config, key):
        """
        Process a beacon configuration to determine its interval
        """

        interval = False
        if isinstance(current_beacon_config, dict):
            interval = current_beacon_config.get(key, False)

        return interval

    def _process_interval(self, mod, interval):
        """
        Process beacons with intervals
        Return True if a beacon should be run on this loop
        """
        log.trace("Processing interval %s for beacon mod %s", interval, mod)
        loop_interval = self.opts["loop_interval"]
        if mod in self.interval_map:
            log.trace("Processing interval in map")
            counter = self.interval_map[mod]
            log.trace("Interval counter: %s", counter)
            if counter * loop_interval >= interval:
                self.interval_map[mod] = 1
                return True
            else:
                self.interval_map[mod] += 1
        else:
            log.trace("Interval process inserting mod: %s", mod)
            self.interval_map[mod] = 1
        return False

    def _get_index(self, beacon_config, label):
        """
        Return the index of a labeled config item in the beacon config, -1 if the index is not found
        """

        indexes = [index for index, item in enumerate(beacon_config) if label in item]
        if not indexes:
            return -1
        else:
            return indexes[0]

    def _remove_list_item(self, beacon_config, label):
        """
        Remove an item from a beacon config list
        """

        index = self._get_index(beacon_config, label)
        del beacon_config[index]

    def _update_enabled(self, name, enabled_value):
        """
        Update whether an individual beacon is enabled
        """

        if isinstance(self.opts["beacons"][name], dict):
            # Backwards compatibility
            self.opts["beacons"][name]["enabled"] = enabled_value
        else:
            enabled_index = self._get_index(self.opts["beacons"][name], "enabled")
            if enabled_index >= 0:
                self.opts["beacons"][name][enabled_index]["enabled"] = enabled_value
            else:
                self.opts["beacons"][name].append({"enabled": enabled_value})

    def _get_beacons(self, include_opts=True, include_pillar=True):
        """
        Return the beacons data structure
        """
        beacons = {}
        if include_pillar:
            pillar_beacons = self.opts.get("pillar", {}).get("beacons", {})
            if not isinstance(pillar_beacons, dict):
                raise ValueError("Beacons must be of type dict.")
            beacons.update(pillar_beacons)
        if include_opts:
            opts_beacons = self.opts.get("beacons", {})
            if not isinstance(opts_beacons, dict):
                raise ValueError("Beacons must be of type dict.")
            beacons.update(opts_beacons)
        return beacons

    def list_beacons(self, include_pillar=True, include_opts=True):
        """
        List the beacon items

        include_pillar: Whether to include beacons that are
                        configured in pillar, default is True.

        include_opts:   Whether to include beacons that are
                        configured in opts, default is True.
        """
        beacons = self._get_beacons(
            include_pillar=include_pillar, include_opts=include_opts
        )

        # Fire the complete event back along with the list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {"complete": True, "beacons": beacons},
                tag="/salt/minion/minion_beacons_list_complete",
            )

        return True

    def list_available_beacons(self):
        """
        List the available beacons
        """
        _beacons = [
            "{}".format(_beacon.replace(".beacon", ""))
            for _beacon in self.beacons
            if ".beacon" in _beacon
        ]

        # Fire the complete event back along with the list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {"complete": True, "beacons": _beacons},
                tag="/salt/minion/minion_beacons_list_available_complete",
            )

        return True

    def validate_beacon(self, name, beacon_data):
        """
        Return available beacon functions
        """
        beacon_name = next(item.get("beacon_module", name) for item in beacon_data)

        validate_str = f"{beacon_name}.validate"
        # Run the validate function if it's available,
        # otherwise there is a warning about it being missing
        if validate_str in self.beacons:
            if "enabled" in beacon_data:
                del beacon_data["enabled"]
            valid, vcomment = self.beacons[validate_str](beacon_data)
        else:
            vcomment = (
                "Beacon {} does not have a validate"
                " function, skipping validation.".format(beacon_name)
            )
            valid = True

        # Fire the complete event back along with the list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {"complete": True, "vcomment": vcomment, "valid": valid},
                tag="/salt/minion/minion_beacon_validation_complete",
            )

        return True

    def add_beacon(self, name, beacon_data):
        """
        Add a beacon item
        """

        data = {}
        data[name] = beacon_data

        if name in self._get_beacons(include_opts=False):
            comment = (
                "Cannot update beacon item {}, "
                "because it is configured in pillar.".format(name)
            )
            complete = False
        else:
            if name in self.opts["beacons"]:
                comment = f"Updating settings for beacon item: {name}"
            else:
                comment = f"Added new beacon item: {name}"
            complete = True
            self.opts["beacons"].update(data)

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts, listen=False) as evt:
            evt.fire_event(
                {
                    "complete": complete,
                    "comment": comment,
                    "beacons": self.opts["beacons"],
                },
                tag="/salt/minion/minion_beacon_add_complete",
            )

        return True

    def modify_beacon(self, name, beacon_data):
        """
        Modify a beacon item
        """

        data = {}
        data[name] = beacon_data

        if name in self._get_beacons(include_opts=False):
            comment = f"Cannot modify beacon item {name}, it is configured in pillar."
            complete = False
        else:
            comment = f"Updating settings for beacon item: {name}"
            complete = True
            self.opts["beacons"].update(data)

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {
                    "complete": complete,
                    "comment": comment,
                    "beacons": self.opts["beacons"],
                },
                tag="/salt/minion/minion_beacon_modify_complete",
            )
        return True

    def delete_beacon(self, name):
        """
        Delete a beacon item
        """

        if name in self._get_beacons(include_opts=False):
            comment = f"Cannot delete beacon item {name}, it is configured in pillar."
            complete = False
        else:
            if name in self.opts["beacons"]:
                # Close the beacon module to release resources (e.g. inotify fds)
                # before removing it from the configuration.
                self._close_beacon(name)
                del self.opts["beacons"][name]
                comment = f"Deleting beacon item: {name}"
            else:
                comment = f"Beacon item {name} not found."
            complete = True

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {
                    "complete": complete,
                    "comment": comment,
                    "beacons": self.opts["beacons"],
                },
                tag="/salt/minion/minion_beacon_delete_complete",
            )

        return True

    def enable_beacons(self):
        """
        Enable beacons
        """

        self.opts["beacons"]["enabled"] = True

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {"complete": True, "beacons": self.opts["beacons"]},
                tag="/salt/minion/minion_beacons_enabled_complete",
            )

        return True

    def disable_beacons(self):
        """
        Enable beacons
        """

        self.opts["beacons"]["enabled"] = False

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {"complete": True, "beacons": self.opts["beacons"]},
                tag="/salt/minion/minion_beacons_disabled_complete",
            )

        return True

    def enable_beacon(self, name):
        """
        Enable a beacon
        """

        if name in self._get_beacons(include_opts=False):
            comment = f"Cannot enable beacon item {name}, it is configured in pillar."
            complete = False
        else:
            self._update_enabled(name, True)
            comment = f"Enabling beacon item {name}"
            complete = True

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {
                    "complete": complete,
                    "comment": comment,
                    "beacons": self.opts["beacons"],
                },
                tag="/salt/minion/minion_beacon_enabled_complete",
            )

        return True

    def disable_beacon(self, name):
        """
        Disable a beacon
        """

        if name in self._get_beacons(include_opts=False):
            comment = (
                "Cannot disable beacon item {}, it is configured in pillar.".format(
                    name
                )
            )
            complete = False
        else:
            self._update_enabled(name, False)
            comment = f"Disabling beacon item {name}"
            complete = True

        # Fire the complete event back along with updated list of beacons
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {
                    "complete": complete,
                    "comment": comment,
                    "beacons": self.opts["beacons"],
                },
                tag="/salt/minion/minion_beacon_disabled_complete",
            )

        return True

    def reset(self):
        """
        Reset the beacons to defaults
        """
        self.opts["beacons"] = {}
        with salt.utils.event.get_event("minion", opts=self.opts) as evt:
            evt.fire_event(
                {
                    "complete": True,
                    "comment": "Beacons have been reset",
                    "beacons": self.opts["beacons"],
                },
                tag="/salt/minion/minion_beacon_reset_complete",
            )
        return True
