import abc
import copy
from collections.abc import MutableMapping

from ansible import constants as C
from ansible.errors import AnsibleParserError, AnsibleRuntimeError, AnsibleError
from ansible.inventory.helpers import get_group_vars
from ansible.module_utils.six import string_types
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.parsing.utils import addresses
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.utils.vars import isidentifier, combine_vars

DOCUMENTATION = """
    name: reconstructed
    short_description: A plugin that allows the dynamic construction of groups
    author: Emmanuel BENOÎT
    description:
    - This inventory plugin allows the construction of groups, the optional
      assignment of hosts to these groups and the computation of arbitrary
      facts.
    options:
      plugin:
        description:
        - Token that ensures this is a source file for the C(group_creator)
          plugin.
        required: True
        choices: ['tseeker.reconstructed.reconstructed','reconstructed']
      instructions:
        description:
        - The list of instructions to be executed in order to generate the
          inventory parts. Each instruction is represented as a dictionnary
          with at least an C(action) field which determines which instruction
          must be executed. The instructions will be executed once for each
          inventory host.
        - Instructions may include various fields that act as control flow.
        - If the C(loop) field is present, it must contain a list (or a Jinja
          template that will return a list). The instruction will be repeated
          for each value in the list. The C(loop_var) field may be added to
          specify the name of the variable into which the current value will
          be written; by default the C(item) variable will be used. Once the
          loop execution ends, the loop variable's previous state is restored.
        - The C(when) field, if present, must contain a Jinja expression
          representing a condition which will be checked before the instruction
          is executed.
        - The C(run_once) field will ensure that the instuction it is attached
          to will only run one time at most.
        - The C(action) field must be set to one of the following values.
        - The C(block) action is another form of flow control, which can be
          used to repeat multiple instructions or make them obey a single
          conditional. The instruction must include a C(block) field, containing
          the list of instructions which are part of the block. In addition, it
          may have a C(rescue) field, containing a list of instructions which
          will be executed on error, and C(always), which may contain a list
          of instructions to execute in all cases. If the C(locals) field is
          defined, it must contain a table of local variables to define. Any
          local variable defined by the instructions under C(block), C(rescue)
          or C(always) will go out of scope once the block finishes executing,
          and the previous values, if any, will be restored.
        - C(create_group) creates a group. The name of the group must be
          provided using the C(group) field, which must be a valid name or a
          Jinja template that evaluates to a valid name. In addition, a
          C(parent) field containting the name of a single, existing parent
          group (or a Jinja template generating the name) may be provided.
          Finally, the C(add_host) field may be set to a truthy value if the
          current host must be added to the new group.
        - C(add_child) adds a child group to another group. The name of the
          group being added must be provided in the C(child) entry, while
          the name of the parent must be provided in the C(group) entry. Both
          groups must exist. In addition, the names may be specified using
          Jinja templates.
        - C(add_host) adds the current inventory host to a group. The name
          of the group must be provided in the C(group) entry. The group
          must exist.
        - C(fail) causes the computations for the current host to stop with
          an error. The error message may be specified in the C(message)
          entry; if present, it will be evaluated using Jinja.
        - C(set_fact) and C(set_var) create a fact and a local variable,
          respectively. Local variables will only be kept during the execution
          of the script for the current host, while facts will be added to the
          host's data. The C(name) entry specifies the name of the fact or
          variable while the C(value) entry specifies its value. Both may be
          Jinja templates.
        - C(stop) stops processing the list of instructions for the current
          host.
        - C(rename_host) changes a host's name. It can only be executed once per
          host.
        type: list
        elements: dict
        required: True
      strictness:
        description:
        - The C(host) setting will cause an error to skip the host being
          processed, and the C(full) setting will abort the execution
          altogether.
        required: False
        choices: ['host', 'full']
        default: host
"""

INSTR_COMMON_FIELDS = ("action", "loop", "loop_var", "run_once", "vars", "when")
"""Fields that may be present on all instructions."""

INSTR_OWN_FIELDS = {
    "add_child": ("group", "child"),
    "add_host": ("group",),
    "block": ("block", "rescue", "always", "locals"),
    "create_group": ("group", "parent", "add_host"),
    "fail": ("msg",),
    "rename_host": ("name",),
    "set_fact": ("name", "value"),
    "set_var": ("name", "value"),
    "stop": (),
}
"""Fields that are specific to each instruction."""

INSTR_FIELDS = {k: set(v + INSTR_COMMON_FIELDS) for k, v in INSTR_OWN_FIELDS.items()}
"""All supported fields for each instruction, including common and specific fields."""


class Context:
    """Script execution context.

    This class contains the script's context when it is executed for a given
    host. This includes the variable storage instance as well as the host's new
    name  if a ``rename_host`` instruction has been executed.
    """

    def __init__(self, host_vars):
        """Initialize the context.

        Args:
            host_vars: the host variables
        """
        self.variables = VariableStorage(host_vars)
        self.new_name = None


class VariableStorage(MutableMapping):
    """Variable storage and cache.

    This class implements storage for local variables, with the ability to save
    some of them and then restore them. It also implements a cache that combines
    both local variables and host facts.
    """

    def __init__(self, host_vars):
        """Initialize the cache using the specified mapping of host variables.

        Args:
            host_vars: the host variables
        """
        self._host_vars = host_vars
        self._script_vars = {}
        self._script_stack = []
        self._cache = host_vars.copy()

    def _script_stack_push(self, variables):
        """Push the state of some local variables to the stack.

        This method will add a record containing the state of some variables to
        the stack so it may be restored later. The state for a single variable
        consists in a flag indicating whether the variable existed or not, and
        its value if it did.

        Args:
            variables: an iterable of variable names whose state must be pushed
        """
        data = {}
        for v in variables:
            if v in self._script_vars:
                se = (True, copy.copy(self._script_vars[v]))
            else:
                se = (False, None)
            data[v] = se
        self._script_stack.append(data)

    def _script_stack_pop(self):
        """Restore the state of local variables from the stack.

        This method will restore state entries that were saved by
        :py:meth:`_script_stack_push`. Local variables that didn't exist then
        will be deleted, while variables which actually existed will be
        restored. The cache will be reset.
        """
        restore = self._script_stack.pop()
        unchanged = 0
        for vn, vv in restore.items():
            existed, value = vv
            if existed:
                self._script_vars[vn] = value
            elif vn in self._script_vars:
                del self._script_vars[vn]
            else:
                unchanged += 1
        if unchanged != len(restore):
            self._cache = self._host_vars.copy()
            self._cache.update(self._script_vars)

    def _set_host_var(self, name, value):
        """Set a host variable.

        This method sets the value of a host variable in the appropriate
        mapping, and updates the cache as will unless a local variable with the
        same name exists.

        Note: the actual inventory is not modified, only the local copy of
        host variables is.

        Args:
            name: the name of the variable
            value: the value of the variable
        """
        self._host_vars[name] = value
        if name not in self._script_vars:
            self._cache[name] = value

    def __getitem__(self, k):
        return self._cache[k]

    def __setitem__(self, k, v):
        self._script_vars[k] = v
        self._cache[k] = v

    def __delitem__(self, k):
        del self._script_vars[k]
        if k in self._host_vars:
            self._cache[k] = self._host_vars[k]
        else:
            del self._cache[k]

    def __iter__(self):
        return self._cache.__iter__()

    def __len__(self):
        return len(self._cache)

    def keys(self):
        return self._cache.keys()

    def items(self):
        return self._cache.items()

    def values(self):
        return self._cache.values()


class RcInstruction(abc.ABC):
    """An instruction that can be executed by the plugin."""

    DEFAULT_LOOP_VAR = "item"
    """The name of the default loop variable."""

    def __init__(self, inventory, templar, display, action):
        self._inventory = inventory
        self._templar = templar
        self._display = display
        self._action = action
        self._condition = None
        self._executed_once = None
        self._loop = None
        self._loop_var = None
        self._vars = {}
        self._save = None

    def __repr__(self):
        """Builds a compact debugging representation of the instruction, \
                including any conditional or iteration clause."""
        flow = []
        if self._condition is not None:
            flow.append("when=%s" % (repr(self._condition),))
        if self._loop is not None:
            flow.append(
                "loop=%s, loop_var=%s" % (repr(self._loop), repr(self._loop_var))
            )
        if len(self._vars) != 0:
            flow.append("vars=%s" % (repr(self._vars),))
        if self._executed_once is not None:
            flow.append("run_once")
        if flow:
            output = "{%s}" % (", ".join(flow),)
        else:
            output = ""
        output += self.repr_instruction_only()
        return output

    def repr_instruction_only(self):
        """Builds a compact debugging representation of the instruction itself."""
        return "%s()" % (self._action,)

    def dump(self):
        """Builds a representation of the instruction over multiple lines.

        This method generates a representation of the instruction, including
        any conditional or iteration clause, over multiple lines. It is meant
        to be used when generating a dump of the parsed program for high
        verbosity values.

        Returns:
            a list of strings (one for each line)
        """
        output = []
        if self._executed_once is not None:
            output.append("{run_once}")
        if self._loop is not None:
            output.append("{loop[%s]: %s}" % (self._loop_var, repr(self._loop)))
        for var in self._vars:
            output.append("{var %s=%s}" % (var, repr(self._vars[var])))
        if self._condition is not None:
            output.append("{when: %s}" % (repr(self._condition),))
        output.extend(self.dump_instruction())
        return output

    def dump_instruction(self):
        """Builds the multi-line debugging representation of the instruction.

        This method returns a list of strings that correspond to the output
        lines that represent the instruction.

        Returns:
            a list of strings (one for each line)
        """
        return [self.repr_instruction_only()]

    def parse(self, record):
        """Parse the instruction's record.

        This method ensures that no unsupported fields are present in the
        instruction's record. It then extracts the conditional clause and the
        iteration clause, if they are present. Finally it calls ``parse_action``
        in order to extract the instruction itself.

        Args:
            record: the dictionnary that contains the instruction
        """
        assert "action" in record and record["action"] == self._action
        # Ensure there are no unsupported fields
        extra_fields = set(record.keys()).difference(INSTR_FIELDS[self._action])
        if extra_fields:
            raise AnsibleParserError(
                "%s: unsupported fields: %s" % (self._action, ", ".join(extra_fields))
            )
        # Extract the loop, condition and local variable clauses
        self.parse_condition(record)
        self.parse_loop(record)
        self._vars = self.parse_vars(record)
        self.parse_run_once(record)
        # Cache the list of variables to save before execution
        save = list(self._vars.keys())
        if self._loop is not None:
            save.append(self._loop_var)
        self._save = tuple(save)
        # Process action-specific fields
        self.parse_action(record)

    def parse_condition(self, record):
        """Parse the ``when`` clause of an instruction.

        If the ``when`` clause is present, ensure it contains a string then
        store it.

        Args:
            record: the YAML data

        Raises:
            AnsibleParserError: if the ``when`` clause is present but does not \
                    contain a string
        """
        if "when" not in record:
            return
        if not isinstance(record["when"], string_types):
            raise AnsibleParserError(
                "%s: 'when' clause is not a string" % (self._action,)
            )
        self._condition = record["when"]

    def parse_loop(self, record):
        """Parse the ``loop`` and ``loop_var`` clauses of an instruction.

        Check for proper usage of both the ``loop`` and ``loop_var`` clauses,
        then extract the values and store them.

        Args:
            record: the instruction's YAML data

        Raises:
            AnsibleParserError: when ``loop_var`` is being used without \
                ``loop``, when the type of either is incorrect, or when the \
                value of ``loop_var`` is not a valid identifier.
        """
        if "loop" not in record:
            if "loop_var" in record:
                raise AnsibleParserError(
                    "%s: 'loop_var' clause found without 'loop'" % (self._action,)
                )
            return
        loop = record["loop"]
        if not isinstance(loop, string_types + (list,)):
            raise AnsibleParserError(
                "%s: 'loop' clause is neither a string nor a list" % (self._action,)
            )
        loop_var = record.get("loop_var", RcInstruction.DEFAULT_LOOP_VAR)
        if not isinstance(loop_var, string_types):
            raise AnsibleParserError(
                "%s: 'loop_var' clause is not a string" % (self._action,)
            )
        if not isidentifier(loop_var):
            raise AnsibleParserError(
                "%s: 'loop_var' value '%s' is not a valid identifier"
                % (self._action, loop_var)
            )
        self._loop = loop
        self._loop_var = loop_var

    def parse_vars(self, record):
        """Parse local variable definitions from the record.

        This method checks for a ``vars`` section in the YAML data, and extracts
        it if it exists.

        Args:
            record: the YAML data for the instruction

        Returns:
            a dictionnary that contains the variable definitions

        Raises:
            AnsibleParserError: when the ``vars`` entry is invalid or contains \
                    invalid definitions
        """
        if "vars" not in record:
            return {}
        if not isinstance(record["vars"], dict):
            raise AnsibleParserError(
                "%s: 'vars' should be a dictionnary" % (self._action,)
            )
        for k, v in record["vars"].items():
            if not isinstance(k, string_types):
                raise AnsibleParserError(
                    "%s: vars identifiers must be strings" % (self._action,)
                )
            if not isidentifier(k):
                raise AnsibleParserError(
                    "%s: '%s' is not a valid identifier" % (self._action, k)
                )
        return record["vars"]

    def parse_run_once(self, record):
        """Parse an instruction's ``run_once`` clause.

        Args:
            record: the YAML data for the instruction

        Raises:
            AnsibleParserError: when the clause is present but does not \
                contain a truthy value
        """
        if "run_once" not in record:
            return
        if not isinstance(record["run_once"], bool):
            raise AnsibleParserError(
                "%s: run_once must be a truthy value" % (self._action,)
            )
        if record["run_once"]:
            self._executed_once = False

    def parse_group_name(self, record, name):
        """Parse a field containing the name of a group, or a template.

        This helper method may be used by implementations to extract either a
        group name or a template from a field. If the string cannot possibly be
        a Jinja template, it will be stripped of extra spaces then checked for
        invalid characters.

        Args:
            record: the dictionnary that contains the instruction
            name: the name of the field to read

        Returns:
            a tuple consisting of a boolean that indicates whether the string
            may be a template or not, and the string itself.
        """
        if name not in record:
            raise AnsibleParserError("%s: missing '%s' field" % (self._action, name))
        group = record[name]
        if not isinstance(group, string_types):
            raise AnsibleParserError(
                "%s: '%s' field must be a string" % (self._action, name)
            )
        may_be_template = self._templar.is_possibly_template(group)
        if not may_be_template:
            group = group.strip()
            if C.INVALID_VARIABLE_NAMES.findall(group):
                raise AnsibleParserError(
                    "%s: invalid group name '%s' in field '%s'"
                    % (self._action, group, name)
                )
        return may_be_template, group

    @abc.abstractmethod
    def parse_action(self, record):
        """Parse the instruction-specific fields.

        This method must be overridden to read all necessary data from the input
        record and configure the instruction.

        Args:
            record: the dictionnary that contains the instruction
        """
        raise NotImplementedError

    def run_for(self, host_name, context):
        """Execute the instruction for a given host.

        This method is the entry point for instruction execution. Depending on
        whether an iteration clause is present or not, it will either call
        :py:meth:`run_iteration` directly or evaluate the loop data then run it
        once for each item, after setting the loop variable.

        Args:
            host_name: the name of the host to execute the instruction for
            context: the execution context

        Returns:
            ``True`` if execution must continue, ``False`` if it must be
            interrupted
        """
        if self._executed_once is True:
            return True
        if self._executed_once is False:
            self._executed_once = True
        # Save previous loop and local variables state
        context.variables._script_stack_push(self._save)
        try:
            # Instructions without loops
            if self._loop is None:
                self._display.vvvv("%s : running action %s" % (host_name, self._action))
                return self.run_iteration(host_name, context)
            # Loop over all values
            for value in self.evaluate_loop(host_name, context.variables):
                self._display.vvvv(
                    "%s : running action %s for item %s"
                    % (host_name, self._action, repr(value))
                )
                context.variables[self._loop_var] = value
                if not self.run_iteration(host_name, context):
                    return False
            return True
        finally:
            # Restore loop variable state
            context.variables._script_stack_pop()

    def run_iteration(self, host_name, context):
        """Check the condition if it exists, then run the instruction.

        Args:
            host_name: the name of the host to execute the instruction for
            context: the execution context

        Returns:
            ``True`` if execution must continue, ``False`` if it must be
            interrupted
        """
        self.compute_locals(context.variables)
        if self.evaluate_condition(host_name, context.variables):
            rv = self.execute_action(host_name, context)
            if not rv:
                self._display.vvvvv(
                    "%s : action %s returned False, stopping"
                    % (host_name, self._action)
                )
        else:
            rv = True
        return rv

    def evaluate_condition(self, host_name, variables):
        """Evaluate the condition for an instruction's execution.

        Args:
            host_name: the name of the host to execute the instruction for
            variables: the variable storage instance

        Returns:
            ``True`` if there is no conditional clause for this instruction, or
            if there is one and it evaluated to a truthy value; ``False``
            otherwise.
        """
        if self._condition is None:
            return True
        t = self._templar
        t.available_variables = variables
        template = "%s%s%s" % (
            t.environment.variable_start_string,
            self._condition,
            t.environment.variable_end_string,
        )
        rv = boolean(t.template(template, disable_lookups=False))
        self._display.vvvvv(
            "host %s, action %s, condition %s evaluating to %s"
            % (host_name, self._action, repr(self._condition), repr(rv))
        )
        return rv

    def compute_locals(self, variables):
        """Compute local variables.

        This method iterates through all local variable definitions and runs
        them through the templar.

        Args:
            variables: the variable storage instance
        """
        self._templar.available_variables = variables
        for key, value in self._vars.items():
            result = self._templar.template(value)
            variables[key] = result
            self._display.vvvv("- set local variable %s to %s" % (key, result))

    def evaluate_loop(self, host_name, variables):
        """Evaluate the values to iterate over when a ``loop`` is defined.

        Args:
            host_name: the name of the host to execute the instruction for
            variables: the variable storage instance

        Returns:
            the list of items to iterate over
        """
        self._display.vvvvv(
            "host %s, action %s, evaluating loop template %s"
            % (host_name, self._action, repr(self._loop))
        )
        self._templar.available_variables = variables
        value = self._templar.template(self._loop, disable_lookups=False)
        if not isinstance(value, list):
            raise AnsibleRuntimeError(
                "template '%s' did not evaluate to a list" % (self._loop,)
            )
        return value

    def get_templated_group(self, variables, may_be_template, name, must_exist=False):
        """Extract a group name from its source, optionally ensure it exists, \
            then return it.

        Args:
            variables: the variable storage instance
            may_be_template: a flag that indicates whether the name should be \
                processed with the templar.
            name: the name or its template
            must_exist: a flag that, if ``True``, will cause an exception to \
                be raised if the group does not exist.

        Returns:
            the name of the group
        """
        if may_be_template:
            self._templar.available_variables = variables
            real_name = self._templar.template(name)
            if not isinstance(real_name, string_types):
                raise AnsibleRuntimeError(
                    "%s: '%s' did not coalesce into a string" % (self._action, name)
                )
            real_name = real_name.strip()
            if C.INVALID_VARIABLE_NAMES.findall(real_name):
                raise AnsibleRuntimeError(
                    "%s: '%s' is not a valid group name" % (self._action, real_name)
                )
        else:
            real_name = name
        if must_exist and real_name not in self._inventory.groups:
            raise AnsibleRuntimeError(
                "%s: group '%s' does not exist" % (self._action, real_name)
            )
        return real_name

    @abc.abstractmethod
    def execute_action(self, host_name, context):
        """Execute the instruction.

        This method must be overridden to implement the actual action of the
        instruction.

        Args:
            host_name: the name of the host to execute the instruction for
            context: the execution context for the current host

        Return:
            ``True`` if the script's execution should continue, ``False`` if
            it should be interrupted.
        """
        raise NotImplementedError


class RciCreateGroup(RcInstruction):
    """``create_group`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "create_group")
        self._group_mbt = None
        self._group_name = None
        self._parent_mbt = None
        self._parent_name = None
        self._add_host = None

    def repr_instruction_only(self):
        output = "%s(group=%s" % (self._action, repr(self._group_name))
        if self._parent_name is not None:
            output += ",parent=" + repr(self._parent_name)
        output += ",add_host=" + repr(self._add_host) + ")"
        return output

    def parse_action(self, record):
        assert self._group_mbt is None and self._group_name is None
        assert self._parent_mbt is None and self._parent_name is None
        assert self._add_host is None
        self._add_host = record.get("add_host", False)
        self._group_mbt, self._group_name = self.parse_group_name(record, "group")
        if "parent" in record:
            self._parent_mbt, self._parent_name = self.parse_group_name(
                record, "parent"
            )

    def execute_action(self, host_name, context):
        assert not (
            self._group_mbt is None
            or self._group_name is None
            or self._add_host is None
        )
        if self._parent_name is not None:
            parent = self.get_templated_group(
                context.variables, self._parent_mbt, self._parent_name, must_exist=True
            )
        name = self.get_templated_group(
            context.variables, self._group_mbt, self._group_name
        )
        self._inventory.add_group(name)
        self._display.vvv("- created group %s" % (name,))
        if self._parent_name is not None:
            self._inventory.add_child(parent, name)
            self._display.vvv("- added group %s to %s" % (name, parent))
        if self._add_host:
            self._inventory.add_child(name, host_name)
            self._display.vvv("- added host %s to %s" % (host_name, name))
        return True


class RciAddHost(RcInstruction):
    """``add_host`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "add_host")
        self._may_be_template = None
        self._group = None

    def repr_instruction_only(self):
        return "%s(group=%s)" % (self._action, repr(self._group))

    def parse_action(self, record):
        assert self._may_be_template is None and self._group is None
        self._may_be_template, self._group = self.parse_group_name(record, "group")

    def execute_action(self, host_name, context):
        assert not (self._may_be_template is None or self._group is None)
        name = self.get_templated_group(
            context.variables, self._may_be_template, self._group, must_exist=True
        )
        self._inventory.add_child(name, host_name)
        self._display.vvv("- added host %s to %s" % (host_name, name))
        return True


class RciAddChild(RcInstruction):
    """``add_child`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "add_child")
        self._group_mbt = None
        self._group_name = None
        self._child_mbt = None
        self._child_name = None

    def repr_instruction_only(self):
        return "%s(group=%s, child=%s)" % (
            self._action,
            repr(self._group_name),
            repr(self._child_name),
        )

    def parse_action(self, record):
        assert self._group_mbt is None and self._group_name is None
        assert self._child_mbt is None and self._child_name is None
        self._group_mbt, self._group_name = self.parse_group_name(record, "group")
        self._child_mbt, self._child_name = self.parse_group_name(record, "child")

    def execute_action(self, host_name, context):
        assert not (self._group_mbt is None or self._group_name is None)
        assert not (self._child_mbt is None or self._child_name is None)
        group = self.get_templated_group(
            context.variables, self._group_mbt, self._group_name, must_exist=True
        )
        child = self.get_templated_group(
            context.variables, self._child_mbt, self._child_name, must_exist=True
        )
        self._inventory.add_child(group, child)
        self._display.vvv("- added group %s to %s" % (child, group))
        return True


class RciSetVarOrFact(RcInstruction):
    """Implementation of the ``set_fact`` and ``set_var`` instructions."""

    def __init__(self, inventory, templar, display, is_fact):
        action = "set_" + ("fact" if is_fact else "var")
        super().__init__(inventory, templar, display, action)
        self._is_fact = is_fact
        self._var_name = None
        self._name_may_be_template = None
        self._var_value = None

    def repr_instruction_only(self):
        return "%s(name=%s, value=%s)" % (
            self._action,
            repr(self._var_name),
            repr(self._var_value),
        )

    def parse_action(self, record):
        assert (
            self._var_name is None
            and self._name_may_be_template is None
            and self._var_value is None
        )
        if "name" not in record:
            raise AnsibleParserError("%s: missing 'name' field" % (self._action,))
        name = record["name"]
        if not isinstance(name, string_types):
            raise AnsibleParserError("%s: 'name' must be a string" % (self._action,))
        if "value" not in record:
            raise AnsibleParserError("%s: missing 'value' field" % (self._action,))
        nmbt = self._templar.is_possibly_template(name)
        if not (nmbt or isidentifier(name)):
            raise AnsibleParserError(
                "%s: '%s' is not a valid variable name" % (self._action, name)
            )
        self._name_may_be_template = nmbt
        self._var_name = name
        self._var_value = record["value"]

    def execute_action(self, host_name, context):
        assert not (
            self._var_name is None
            or self._name_may_be_template is None
            or self._var_value is None
        )
        self._templar.available_variables = context.variables
        if self._name_may_be_template:
            name = self._templar.template(self._var_name)
            if not isinstance(name, string_types):
                raise AnsibleRuntimeError(
                    "%s: '%s' did not coalesce into a string"
                    % (self._action, self._var_name)
                )
            if not isidentifier(name):
                raise AnsibleRuntimeError(
                    "%s: '%s' is not a valid variable name" % (self._action, name)
                )
        else:
            name = self._var_name
        value = self._templar.template(self._var_value)
        if self._is_fact:
            self._inventory.set_variable(host_name, name, value)
            context.variables._set_host_var(name, value)
        else:
            context.variables[name] = value
        self._display.vvv(
            "- set %s %s to %s"
            % ("fact" if self._is_fact else "var", name, repr(value))
        )
        return True


class RciStop(RcInstruction):
    """``stop`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "stop")

    def parse_action(self, record):
        pass

    def execute_action(self, host_name, context):
        self._display.vvv("- stopped execution")
        return False


class RciFail(RcInstruction):
    """``fail`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "fail")
        self._message = None

    def repr_instruction_only(self):
        if self._message is None:
            return "%s()" % (self._action,)
        else:
            return "%s(%s)" % (self._action, self._message)

    def parse_action(self, record):
        self._message = record.get("msg", None)

    def execute_action(self, host_name, context):
        if self._message is None:
            message = "fail requested (%s)" % (host_name,)
        else:
            self._templar.available_variables = context.variables
            message = self._templar.template(self._message)
        self._display.vvv("- failed with message %s" % (message,))
        raise AnsibleRuntimeError(message)


class RciRenameHost(RcInstruction):
    """``rename_host`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "rename_host")
        self._name = None
        self._name_may_be_template = None

    def parse_action(self, record):
        assert self._name is None and self._name_may_be_template is None
        if "name" not in record:
            raise AnsibleParserError("%s: missing 'name' field" % (self._action,))
        name = record["name"]
        if not isinstance(name, string_types):
            raise AnsibleParserError("%s: 'name' must be a string" % (self._action,))
        nmbt = self._templar.is_possibly_template(name)
        if not (nmbt or addresses.patterns['hostname'].match(name)):
            raise AnsibleParserError(
                "%s: '%s' is not a valid host name" % (self._action, name)
            )
        self._name_may_be_template = nmbt
        self._name = name

    def execute_action(self, host_name, context):
        if context.new_name is not None:
            raise AnsibleRuntimeError("Host has already been renamed")
        if self._name_may_be_template:
            self._templar.available_variables = context.variables
            name = self._templar.template(self._name)
            if not addresses.patterns['hostname'].match(name):
                raise AnsibleRuntimeError(
                    "%s: '%s' is not a valid host name" % (self._action, name)
                )
        else:
            name = self._name
        self._display.vvv("- renaming host %s to %s" % (host_name, name))
        context.new_name = name
        return True


class RciBlock(RcInstruction):
    """``block`` instruction implementation."""

    def __init__(self, inventory, templar, display):
        super().__init__(inventory, templar, display, "block")
        self._block = None
        self._rescue = None
        self._always = None

    def repr_instruction_only(self):
        return "%s(block=%s, rescue=%s, always=%s)" % (
            self._action,
            repr(self._block),
            repr(self._rescue),
            repr(self._always),
        )

    def dump_instruction(self):
        output = ["%s(...):" % (self._action,)]
        self.dump_section(output, "block", self._block)
        self.dump_section(output, "rescue", self._rescue)
        self.dump_section(output, "always", self._always)
        return output

    def dump_section(self, output, section_name, section_contents):
        """Dump one of the sections.

        This method is used to create the dump that corresponds to one of the
        ``block``, ``rescue`` or ``always`` lists of instructions.

        Args:
            output: a list of strings to append to
            block_name: the name of the section being dumped
            block_contents: the list of instructions in this section
        """
        if not section_contents:
            return
        output.append("  " + section_name + ":")
        for pos, instr in enumerate(section_contents):
            if pos != 0:
                output.append("")
            output.extend("    " + s for s in instr.dump())

    def parse_action(self, record):
        assert self._block is None and self._rescue is None and self._always is None
        if "block" not in record:
            raise AnsibleParserError("%s: missing 'block' field" % (self._action,))
        self._block = self.parse_block(record, "block")
        if "rescue" in record:
            self._rescue = self.parse_block(record, "rescue")
        else:
            self._rescue = []
        if "always" in record:
            self._always = self.parse_block(record, "always")
        else:
            self._always = []

    def parse_block(self, record, key):
        """Parse the contents of one of the instruction lists.

        This method will extract the instructions for one of the ``block``,
        ``rescue`` and ``always`` sections. The corresponding key must exist
        in the YAML data when the method is called. It will ensure that it is
        a list before reading the instructions it contains.

        Args:
            record: the record of the ``block`` instruction
            key: the section to read (``block``, ``rescue`` or ``always``)

        Returns:
            the list of instructions in the section.
        """
        if not isinstance(record[key], list):
            raise AnsibleParserError(
                "%s: '%s' field must contain a list of instructions"
                % (self._action, key)
            )
        instructions = []
        for record in record[key]:
            instructions.append(
                parse_instruction(self._inventory, self._templar, self._display, record)
            )
        return instructions

    def execute_action(self, host_name, context):
        assert not (self._block is None or self._rescue is None or self._always is None)
        try:
            try:
                self._display.vvv("- running 'block' instructions")
                return self.run_section(self._block, host_name, context)
            except AnsibleError as e:
                if not self._rescue:
                    self._display.vvv("- block failed")
                    raise
                self._display.vvv("- block failed, running 'rescue' instructions")
                context.variables["reconstructed_error"] = str(e)
                return self.run_section(self._rescue, host_name, context)
        finally:
            self._display.vvv("- block exited, running 'always' instructions")
            self.run_section(self._always, host_name, context)

    def run_section(self, section, host_name, context):
        """Execute a single section.

        This method executes the sequence of instructions in a single section.

        Args:
            section: the list of instructions
            host_name: the name of the host being processed
            context: the execution context for the current host

        Returns:
            ``True`` if the script's execution should continue, ``False`` if it
            should be interrupted
        """
        for instruction in section:
            if not instruction.run_for(host_name, context):
                return False
        return True


INSTRUCTIONS = {
    "add_child": RciAddChild,
    "add_host": RciAddHost,
    "block": RciBlock,
    "create_group": RciCreateGroup,
    "fail": RciFail,
    "rename_host": RciRenameHost,
    "set_fact": lambda i, t, d: RciSetVarOrFact(i, t, d, True),
    "set_var": lambda i, t, d: RciSetVarOrFact(i, t, d, False),
    "stop": RciStop,
}


def parse_instruction(inventory, templar, display, record):
    action = record["action"]
    if action not in INSTRUCTIONS:
        raise AnsibleParserError("Unknown action '%s'" % (action,))
    instruction = INSTRUCTIONS[action](inventory, templar, display)
    instruction.parse(record)
    return instruction


class InventoryModule(BaseInventoryPlugin):
    """Constructs groups based on lists of instructions."""

    NAME = "tseeker.reconstructed.reconstructed"

    def verify_file(self, path):
        return super().verify_file(path) and path.endswith((".yaml", ".yml"))

    def parse(self, inventory, loader, path, cache=True):
        super().parse(inventory, loader, path, cache)
        self._read_config_data(path)
        # Read the program
        instr_src = self.get_option("instructions")
        instructions = []
        for record in instr_src:
            instructions.append(
                parse_instruction(self.inventory, self.templar, self.display, record)
            )
        self.dump_program(instructions)
        # Execute it for each host
        rename = []
        for host in inventory.hosts:
            self.display.vvv("executing reconstructed script for %s" % (host,))
            try:
                new_name = self.exec_for_host(host, instructions)
            except AnsibleError as e:
                if self.get_option("strictness") == "full":
                    raise
                self.display.warning(
                    "reconstructed - error on host %s: %s" % (host, repr(e))
                )
                continue
            if new_name is not None:
                rename.append((host, new_name))
        # Rename hosts
        for old_name, new_name in rename:
            if old_name.lower() == new_name.lower():
                continue
            self.rename_host(old_name, new_name)

    def exec_for_host(self, host, instructions):
        """Execute the program for a single host.

        This method initialises a variable storage instance from the host's
        variables then runs the instructions.

        Args:
            host: the name of the host to execute for
            instructions: the list of instructions to execute
        """
        host_obj = self.inventory.get_host(host)
        host_vars = host_obj.get_vars()
        host_groups = host_obj.get_groups().copy()
        if not any(g.name == "all" for g in host_groups):
            host_groups.append(self.inventory.groups["all"])
        group_vars = get_group_vars(host_groups)
        context = Context(combine_vars(group_vars, host_vars))
        for instruction in instructions:
            if not instruction.run_for(host, context):
                break
        return context.new_name

    def dump_program(self, instructions):
        """Dump the whole program to the log, depending on verbosity level.

        This method will dump the program to the log. If verbosity is at level
        3, the dump will be written using `repr`. If it is 4 or higher, it will
        be dumped in a much more readable, albeit longer,  form.

        Args:
            instructions: the list of instructions in the program
        """
        if self.display.verbosity < 4:
            if self.display.verbosity == 3:
                self.display.vvv("parsed program: " + repr(instructions))
            return
        output = []
        for pos, instr in enumerate(instructions):
            if pos:
                output.append("")
            output.extend(instr.dump())
        self.display.vvvv("parsed program:\n\n" + "\n".join("  " + s for s in output))

    def rename_host(self, old_name, new_name):
        """Renames a host.

        This method "renames" a host by removing the host's current inventory
        record and creating a new one with the same data, except for the name.

        Args:
            old_name: the host's old name
            new_name: the host's new name
        """
        if self.inventory.get_host(new_name) is not None:
            raise AnsibleRuntimeError("duplicate host name %s" % (new_name,))
        self.display.vvv("renaming host %s to %s" % (old_name, new_name))
        host = self.inventory.get_host(old_name)
        group_names = []
        for g in host.get_groups():
            if g.name == 'all':
                continue
            if old_name in g.host_names:
                group_names.append(g.name)
        self.inventory.remove_host(host)
        self.inventory.add_host(new_name)
        new_host = self.inventory.get_host(new_name)
        new_host._uuid = host._uuid
        if host.address == host.name:
            new_host.address = new_name
        new_host.vars = host.vars
        for group in group_names:
            self.inventory.add_child(group, new_name)