import functools

from .state import GuardStateDecorator


class NodeError(Exception):
    pass


def node_checker(node_manager, fail_return=None):
    """Return GuardStateDecorator for checking fault status of Nodes.

    `node_manager` is a Node or NodeManager object with a
    check_fault() method.  Returns a GuardStateDecorator with it's
    pre_exec method set to be the check_fault method.  The
    "fail_return" option should specify an alternate return value for
    the decorated state method in case the check fails (i.e. a jump
    state name) (default None).

    """
    # we want to return a GuardStateDecorator here, where the
    # pre_exec function is the NodeManager.check method.  since
    # "self" here is the NodeManager, we tell pre_exec to just
    # ignore it's argument, which would have been the
    # GuardStateDecorator self.
    class checker(GuardStateDecorator):
        def pre_exec(__):
            if node_manager.check_fault():
                return fail_return
    return checker


# FIXME: handle rejected requests
# FIXME: check for dead node

class Node(object):
    """Manager interface to a single Guardian node.

    >>> SUS_ETMX = Node('SUS_ETMX')  # create the node object
    >>> SUS_ETMX.init()              # initialize (handled automatically in daemon)
    >>> SUS_ETMX.set_managed()       # set node to be in MANAGED mode
    >>> SUS_ETMX.set_request('DAMPED') # request DAMPED state from node
    >>> SUS_ETMX.arrived             # True if node arrived at requested state
    >>> SUS_ETMX.check_fault()       # Check for management-related "faults" in the Node
    >>> SUS_ETMX.release()           # release node from management

    """

    # node attributes
    attrs=('OP',
           'MODE',
           'MANAGER',
           'REQUEST',
           'STATE_S',
           'TARGET_S',
           'REQUEST_S',
           'STATUS',
           'STALLED',
           'ERROR',
           'NOTIFICATION',
           )

    def __init__(self, name):
        self.__name = name
        self.__prefix = ':GRD-{name}_'.format(name=self.name)
        # cache that we have set node to be managed
        self.__managed = False
        # cache of requested state
        self.__request = None
        # initialization state
        self.__initialized = False

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.__name)

    def __str__(self):
        if self.__initialized:
            s = 'initialized'
        else:
            s = 'uninitialized'
        return "<%s '%s', %s>" % (self.__class__.__name__, self.__name, s)

    @property
    def name(self):
        """Node name"""
        return self.__name

    def __get(self, attr):
        """get attribute of node."""
        return ezca.read(self.__prefix+attr, as_string=True)

    def __put(self, attr, value):
        """set attribute of node."""
        assert attr in ['MODE', 'MANAGER', 'REQUEST'], "MANAGER, MODE and REQUEST are the only settable node attributes."
        return ezca.write(self.__prefix+attr, value)

    ########################################

    def init(self):
        """Initialize the node.

        Under normal circumstances, i.e. in a running guardian daemon,
        node initialization is handled automatically.  This function
        therefore does not need to be executed in user code.

        """
        # FIXME: could elliminate this enitrely by just using the rote
        # ezca object
        if self.__initialized:
            return

        # initialize node PVs
        for attr in self.attrs:
            ezca.connect(self.__prefix+attr)

        # if the node is currently set to be managed by me
        if self.i_manage:

            # update the internal manage indicator
            self.__managed = True

            # set the internal request indicator based on the current
            # request (assuming it was set by me)
            if not self.__request:
                self.__request = self.REQUEST

        self.__initialized = True

    def __eq__(self, state):
        """True if node state string equals string."""
        return self.state == state

    def __ne__(self, state):
        """True if node state string does not equal string."""
        return not self.__eq__(state)

    @property
    def OP(self):
        """node OP"""
        return self.__get('OP')

    @property
    def MODE(self):
        """node MODE"""
        return self.__get('MODE')

    @property
    def managed(self):
        """True if node is MANAGED"""
        return self.MODE == 'MANAGED'

    @property
    def MANAGER(self):
        """MANAGER string of node"""
        return self.__get('MANAGER')

    def set_managed(self):
        """Set node to be managed by this manager."""
        # note that we have set node to be managed
        self.__managed = True
        self.__put('MANAGER', _SYSTEM_)

    def release(self):
        """Release node from management by this manager (MODE=>AUTO)."""
        log("Releasing node from management: %s" % self.name)
        self.__put('MODE', 'AUTO')
        self.__managed = False

    @property
    def i_manage(self):
        """True if node is being managed by this system"""
        return self.MANAGER == _SYSTEM_

    @property
    def ERROR(self):
        """True if node in ERROR."""
        return eval(self.__get('ERROR'))

    @property
    def NOTIFICATION(self):
        """True if node NOTIFICATION present."""
        return eval(self.__get('NOTIFICATION'))

    @property
    def OK(self):
        """Current OK status of node."""
        return eval(self.__get('OK'))

    @property
    def REQUEST(self):
        """Current REQUEST state of node."""
        return self.__get('REQUEST_S')
    request = REQUEST

    def set_request(self, state):
        """Set REQUEST state for node.

        """
        assert state != None
        self.__put('REQUEST', state)
        # HACK: We explicitly get the value without relying on the
        # monitor (use_monitor=False) to make sure we have the
        # post-update value.  If it doesn't match what we put then the
        # value was invalid.  Should be a better way to do this.
        pv = ezca.connect(self.__prefix+'REQUEST')
        if pv.get(use_monitor=False) != state:
            raise NodeError("Invalid REQUEST: {}".format(state))
        self.__request = state
        # FIXME: set call back to record when subordinate makes it to
        # request, for benefit of arrived.

    @property
    def STATE(self):
        """Current STATE of node."""
        return self.__get('STATE_S')
    state = STATE

    @property
    def TARGET(self):
        """Current TARGET state of node."""
        return self.__get('TARGET_S')

    @property
    def arrived(self):
        """True if node STATE equals the last manager-requested state.

        NOTE: This will be False if STATE == REQUEST but REQUEST was
        not last set by this Node manager object.  This prevents false
        positives in the case that the REQUEST has been changed out of
        band.

        """
        # FIXME: Maybe this should be a latching value?  It could be
        # reset when the request is set, and set True once it arrives,
        # and then not change when it looses the requested state.
        # Would need some sort of request callback for this.

        # assume true if no request has ever been issued for this
        # node.
        if not self.__request:
            return True
        else:
            return self.STATE == self.__request

    @property
    def STATUS(self):
        """Current STATUS of node."""
        return self.__get('STATUS')

    @property
    def done(self):
        """True if STATUS is DONE.

        A state is DONE if it is the requested state and the state
        method has returned True.

        """
        return self.STATUS == 'DONE'

    @property
    def completed(self):
        """True is node has arrived at the request state, and state is done.

        """
        return self.arrived and self.done

    @property
    def STALLED(self):
        """True if the node has stalled in the current state.

        This is true when STATE == TARGET != REQUEST, which is
        typically the result of a jump transition while in managed
        mode.

        """
        return eval(self.__get('STALLED'))

    def revive(self):
        """Re-request last requested state.

        The last requested state in this case is the one requested
        from this Node object.

        Useful for reviving stalled nodes, basically counteracting the
        stalling that is the effect of a jump transition while being
        in MANAGED mode.  See the 'STALLED' property.

        """
        if self.__request and self.STALLED:
            self.set_request(self.__request)

    def check_fault(self):
        """Return fault status of node.

        Runs a series of checks on the "management status" of the
        node, and returns True if any of the following checks fail:
         * node still alive and running
         * node does not show ERROR status
         * REQUEST hasn't deviated from last set value
         * if node had been set MANAGED, it is still set, and MANAGER
           hasn't changed
         * node has no notifications (failure does not produce fault)

        Any failure of the above also produces a NOTIFICATION message.

        """
        fault = False

        def snotify(msg):
            notify("{}: {}".format(self.name, msg))

        if self.NOTIFICATION:
            snotify("has notification")

        if self.ERROR:
            snotify("ERROR!")
            fault = True

        if self.OP != 'EXEC':
            snotify("not EXEC")
            fault = True

        if self.__request:
            request = self.REQUEST
            if request != self.__request:
                snotify("REQUEST CHANGED (was: %s, now: %s)" % (self.__request, request))
                fault = True

        if self.__managed:
            if not self.managed:
                snotify("NOT IN MANAGED MODE")
                fault = True

            # if we're not listed as manager
            elif not self.i_manage:
                snotify("STOLEN (by: %s)" % self.MANAGER)
                fault = True

        return fault

    @functools.wraps(node_checker)
    def checker(self, fail_return=None):
        return node_checker(self, fail_return=fail_return)


class NodeManager(object):
    """Manager interface to a set of subordinate Guardian nodes.

    This should be instantiated with a list of node names to be
    managed.  Node objects are instantiated for each node.

    >>> nodes = NodeManager(['SUS_ITMX','SUS_ETMX'])
    >>> nodes.init()                   # initialize (handled automatically in daemon)
    >>> nodes.set_managed()            # set all nodes to be in MANAGED mode
    >>> nodes['SUS_ETMX'] = 'ALIGNED'  # request state of node
    >>> nodes['SUS_ITMX'] = 'ALIGNED'  # request state of node
    >>> nodes.arrived                  # True if all nodes have arrived at their
                                       # requested states
    >>> nodes.check_fault()            # Check for management-related "faults" in all nodes

    """
    def __init__(self, nodes):
        self.nodes = {}
        for node in nodes:
            self.nodes[node] = Node(node)

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.nodes.keys())

    def __str__(self):
        return "<%s %s>" % (self.__class__.__name__, self.nodes.keys())

    def __getitem__(self, node):
        """Retrieve Node object for named node."""
        return self.nodes[node]

    def __contains__(self, node):
        """True if manager contains node."""
        if isinstance(node, Node):
            return node.name in self.nodes
        else:
            return node in self.nodes

    def __setitem__(self, node, state):
        """Request state for named node."""
        try:
            self.nodes[node].set_request(state)
        except NodeError as e:
            raise NodeError("%s: %s" % (node, e))

    def __iter__(self):
        """Iterator of node objects."""
        for node in self.nodes.values():
            yield node

    def init(self):
        """Initialize all nodes.

        Under normal circumstances, i.e. in a running guardian daemon,
        node initialization is handled automatically.  This function
        therefore does not need to be executed in user code.

        """
        for node in self:
            node.init()

    def set_managed(self, nodes=None):
        """Set all nodes to be managed by this manager.

        `names` can be a list of node names to set managed.

        """
        if nodes:
            nl = [self[node] for node in nodes]
        else:
            nl = self
        for node in nl:
            node.set_managed()

    def release(self, nodes=None):
        """Release all nodes from management by this manager.

        `nodes` can be a list of node names to release.

        """
        if nodes:
            nl = [self[node] for node in nodes]
        else:
            nl = self
        for node in nl:
            node.release()

    @property
    def arrived(self):
        """Return True if all nodes have arrived at their requested state."""
        for node in self:
            if not node.arrived:
                return False
        return True

    @property
    def completed(self):
        """Return True if all nodes are arrived and done."""
        for node in self:
            if not node.completed:
                return False
        return True

    def get_stalled_nodes(self):
        """Return a list of all stalled nodes."""
        return [node for node in self if node.STALLED]

    def revive_all(self):
        """Revive all stalled nodes."""
        for node in self:
            if node.STALLED:
                log("Reviving stalled node: %s" % node.name)
                node.revive()

    def not_ok(self):
        """Return set of node names not currently reporting OK status."""
        notok = set()
        for node in self:
            if not node.OK:
                notok.add(node.name)
        return notok

    def check_fault(self):
        """Check fault status of all nodes.

        Runs check_fault() method for all nodes.  Returns True if any
        nodes are in fault.

        """
        any_fault = False
        for node in self:
            any_fault |= node.check_fault()
        return any_fault

    @functools.wraps(node_checker)
    def checker(self, fail_return=None):
        return node_checker(self, fail_return=fail_return)
