from __future__ import annotations

import types
import asyncio
import logging
import weakref
import contextlib
import collections

import synapse.exc as s_exc
import synapse.common as s_common

import synapse.lib.coro as s_coro
import synapse.lib.base as s_base
import synapse.lib.node as s_node
import synapse.lib.cache as s_cache
import synapse.lib.layer as s_layer
import synapse.lib.storm as s_storm
import synapse.lib.types as s_types
import synapse.lib.spooled as s_spooled

logger = logging.getLogger(__name__)

class Snap(s_base.Base):
    '''
    A "snapshot" is a transaction across multiple Cortex layers.

    The Snap object contains the bulk of the Cortex API to
    facilitate performance through careful use of transaction
    boundaries.

    Transactions produce the following EventBus events:

    (...any splice...)
    ('log', {'level': 'mesg': })
    ('print', {}),
    '''

    async def __anit__(self, view, user):
        '''
        Args:
            core (cortex):  the cortex
            layers (List[Layer]): the list of layers to access, write layer last
        '''
        await s_base.Base.__anit__(self)

        self.stack = contextlib.ExitStack()

        assert user is not None

        self.strict = True
        self.elevated = False
        self.canceled = False

        self.core = view.core
        self.view = view
        self.user = user

        self.buidprefetch = self.view.isafork()

        self.layers = list(reversed(view.layers))
        self.wlyr = self.layers[-1]

        self.readonly = self.wlyr.readonly

        # variables used by the storm runtime
        self.vars = {}

        self.runt = {}

        self.debug = False      # Set to true to enable debug output.
        self.write = False      # True when the snap has a write lock on a layer.

        self._tagcachesize = 10000
        self._buidcachesize = 100000
        self.tagcache = s_cache.FixedCache(self._addTagNode, size=self._tagcachesize)
        # Keeps alive the most recently accessed node objects
        self.buidcache = collections.deque(maxlen=self._buidcachesize)
        self.livenodes = weakref.WeakValueDictionary()  # buid -> Node
        self._warnonce_keys = set()

        self.onfini(self.stack.close)
        self.changelog = []
        self.tagtype = self.core.model.type('ival')
        self.trigson = self.core.trigson

    def disableTriggers(self):
        self.trigson = False

    async def getSnapMeta(self):
        '''
        Retrieve snap metadata to store along side nodeEdits.
        '''
        meta = {
            'time': s_common.now(),
            'user': self.user.iden
        }

        providen = self.core.provstor.precommit()
        if providen is not None:
            meta['prov'] = providen

        return meta

    @contextlib.contextmanager
    def getStormRuntime(self, query, opts=None, user=None):
        if user is None:
            user = self.user

        runt = s_storm.Runtime(query, self, opts=opts, user=user)

        yield runt

    async def iterStormPodes(self, text, opts=None, user=None):
        '''
        Yield packed node tuples for the given storm query text.
        '''
        if user is None:
            user = self.user

        dorepr = False
        dopath = False

        self.core._logStormQuery(text, user)

        if opts is not None:
            dorepr = opts.get('repr', False)
            dopath = opts.get('path', False)

        async for node, path in self.storm(text, opts=opts, user=user):
            pode = node.pack(dorepr=dorepr)
            pode[1]['path'] = path.pack(path=dopath)
            yield pode

    @s_coro.genrhelp
    async def storm(self, text, opts=None, user=None):
        '''
        Execute a storm query and yield (Node(), Path()) tuples.
        '''
        if user is None:
            user = self.user

        if opts is None:
            opts = {}

        mode = opts.get('mode', 'storm')

        query = self.core.getStormQuery(text, mode=mode)
        with self.getStormRuntime(query, opts=opts, user=user) as runt:
            async for x in runt.execute():
                yield x

    @s_coro.genrhelp
    async def eval(self, text, opts=None, user=None):
        '''
        Run a storm query and yield Node() objects.
        '''
        if user is None:
            user = self.user

        if opts is None:
            opts = {}

        mode = opts.get('mode', 'storm')

        # maintained for backward compatibility
        query = self.core.getStormQuery(text, mode=mode)
        with self.getStormRuntime(query, opts=opts, user=user) as runt:
            async for node, path in runt.execute():
                yield node

    async def nodes(self, text, opts=None, user=None):
        return [n async for n in self.eval(text, opts=opts, user=user)]

    async def clearCache(self):
        self.tagcache.clear()
        self.buidcache.clear()
        self.livenodes.clear()

    async def printf(self, mesg):
        await self.fire('print', mesg=mesg)

    async def warn(self, mesg, log=True, **info):
        if log:
            logger.warning(mesg)
        await self.fire('warn', mesg=mesg, **info)

    async def warnonce(self, mesg, log=True, **info):
        if mesg in self._warnonce_keys:
            return
        self._warnonce_keys.add(mesg)
        await self.warn(mesg, log, **info)

    async def getNodeByBuid(self, buid):
        '''
        Retrieve a node tuple by binary id.

        Args:
            buid (bytes): The binary ID for the node.

        Returns:
            Optional[s_node.Node]: The node object or None.

        '''
        return await self._joinStorNode(buid, {})

    async def getNodeByNdef(self, ndef):
        '''
        Return a single Node by (form,valu) tuple.

        Args:
            ndef ((str,obj)): A (form,valu) ndef tuple.  valu must be
            normalized.

        Returns:
            (synapse.lib.node.Node): The Node or None.
        '''
        buid = s_common.buid(ndef)
        return await self.getNodeByBuid(buid)

    async def nodesByTagProp(self, form, tag, name):
        prop = self.core.model.getTagProp(name)
        if prop is None:
            mesg = f'No tag property named {name}'
            raise s_exc.NoSuchTagProp(name=name, mesg=mesg)

        for layr in self.layers:
            genr = layr.liftByTagProp(form, tag, name)
            async for node in self._joinStorGenr(layr, genr):
                yield node

    async def nodesByTagPropValu(self, form, tag, name, cmpr, valu):

        prop = self.core.model.getTagProp(name)
        if prop is None:
            mesg = f'No tag property named {name}'
            raise s_exc.NoSuchTagProp(name=name, mesg=mesg)

        cmprvals = prop.type.getStorCmprs(cmpr, valu)
        # an empty return probably means ?= with invalid value
        if not cmprvals:
            return

        for layr in self.layers:
            genr = layr.liftByTagPropValu(form, tag, name, cmprvals)
            async for node in self._joinStorGenr(layr, genr):
                if node.bylayer['tagprops'].get((tag, prop.name)) != layr:
                    continue
                yield node

    async def _joinStorNode(self, buid, cache):

        node = self.livenodes.get(buid)
        if node is not None:
            await asyncio.sleep(0)
            return node

        ndef = None
        tags = {}
        props = {}
        nodedata = {}
        tagprops = {}

        bylayer = {
            'ndef': None,
            'tags': {},
            'props': {},
            'tagprops': {},
        }

        for layr in self.layers:

            sode = cache.get(layr.iden)
            if sode is None:
                sode = await layr.getStorNode(buid)

            form = sode.get('form')
            valt = sode.get('valu')
            if valt is not None:
                ndef = (form, valt[0])
                bylayer['ndef'] = layr

            storprops = sode.get('props')
            if storprops is not None:
                for prop, (valu, stype) in storprops.items():
                    props[prop] = valu
                    bylayer['props'][prop] = layr

            stortags = sode.get('tags')
            if stortags is not None:
                tags.update(stortags)
                bylayer['tags'].update({p: layr for p in stortags.keys()})

            stortagprops = sode.get('tagprops')
            if stortagprops is not None:
                for tagprop, (valu, stype) in stortagprops.items():
                    tagprops[tagprop] = valu
                    bylayer['tagprops'][tagprop] = layr

            stordata = sode.get('nodedata')
            if stordata is not None:
                nodedata.update(stordata)

        if ndef is None:
            return None

        pode = (buid, {
            'ndef': ndef,
            'tags': tags,
            'props': props,
            'nodedata': nodedata,
            'tagprops': tagprops,
        })

        node = s_node.Node(self, pode, bylayer=bylayer)
        self.livenodes[buid] = node
        self.buidcache.append(node)

        # moved here from getNodeByBuid() to cover more
        await asyncio.sleep(0)
        return node

    async def _joinStorGenr(self, layr, genr):
        cache = {}
        async for buid, sode in genr:
            cache[layr.iden] = sode
            node = await self._joinStorNode(buid, cache)
            if node is not None:
                yield node

    async def nodesByDataName(self, name):
        for layr in self.layers:
            genr = layr.liftByDataName(name)
            async for node in self._joinStorGenr(layr, genr):
                yield node

    async def nodesByProp(self, full):

        prop = self.core.model.prop(full)
        if prop is None:
            mesg = f'No property named "{full}".'
            raise s_exc.NoSuchProp(mesg=mesg)

        if prop.isrunt:

            async for node in self.getRuntNodes(prop.full):
                yield node

            return

        if prop.isform:

            for layr in self.layers:
                genr = layr.liftByProp(prop.name, None)

                async for node in self._joinStorGenr(layr, genr):

                    # TODO merge sort rather than use bylayer
                    if node.bylayer.get('ndef') != layr:
                        continue

                    yield node
            return

        if prop.isuniv:

            for layr in self.layers:
                genr = layr.liftByProp(None, prop.name)
                async for node in self._joinStorGenr(layr, genr):
                    # TODO should these type of filters yield?
                    if node.bylayer['props'].get(prop.name) != layr:
                        continue
                    yield node
            return

        formname = None
        if not prop.isuniv:
            formname = prop.form.name

        # Prop is secondary prop

        for layr in self.layers:
            genr = layr.liftByProp(formname, prop.name)
            async for node in self._joinStorGenr(layr, genr):
                if node.bylayer['props'].get(prop.name) != layr:
                    continue
                yield node

    async def nodesByPropValu(self, full, cmpr, valu):

        if cmpr == 'type=':
            async for node in self.nodesByPropValu(full, '=', valu):
                yield node

            async for node in self.nodesByPropTypeValu(full, valu):
                yield node
            return

        prop = self.core.model.prop(full)
        if prop is None:
            mesg = f'No property named "{full}".'
            raise s_exc.NoSuchProp(mesg=mesg)

        cmprvals = prop.type.getStorCmprs(cmpr, valu)
        # an empty return probably means ?= with invalid value
        if not cmprvals:
            return

        if prop.isrunt:
            for storcmpr, storvalu, _ in cmprvals:
                async for node in self.getRuntNodes(prop.full, valu=storvalu, cmpr=storcmpr):
                    yield node
            return

        if prop.isform:

            for layr in self.layers:
                genr = layr.liftByFormValu(prop.name, cmprvals)

                async for node in self._joinStorGenr(layr, genr):

                    # TODO merge sort rather than use bylayer
                    if node.bylayer.get('ndef') != layr:
                        continue

                    yield node

            return

        if prop.isuniv:

            for layr in self.layers:
                genr = layr.liftByPropValu(None, prop.name, cmprvals)
                async for node in self._joinStorGenr(layr, genr):
                    if node.bylayer['props'].get(prop.name) != layr:
                        continue
                    yield node

            return

        for layr in self.layers:
            genr = layr.liftByPropValu(prop.form.name, prop.name, cmprvals)
            async for node in self._joinStorGenr(layr, genr):
                if node.bylayer['props'].get(prop.name) != layr:
                    continue
                yield node

    async def nodesByTag(self, tag, form=None):
        for layr in self.layers:
            genr = layr.liftByTag(tag, form=form)
            async for node in self._joinStorGenr(layr, genr):
                if node.bylayer['tags'].get(tag) != layr:
                    continue
                yield node

    async def nodesByTagValu(self, tag, cmpr, valu, form=None):
        norm, info = self.core.model.type('ival').norm(valu)
        for layr in self.layers:
            genr = layr.liftByTagValu(tag, cmpr, norm, form=form)
            async for node in self._joinStorGenr(layr, genr):
                if node.bylayer['tags'].get(tag) != layr:
                    continue
                yield node

    async def nodesByPropTypeValu(self, name, valu):

        _type = self.core.model.types.get(name)
        if _type is None:
            raise s_exc.NoSuchType(name=name)

        for prop in self.core.model.getPropsByType(name):
            async for node in self.nodesByPropValu(prop.full, '=', valu):
                yield node

    async def nodesByPropArray(self, full, cmpr, valu):

        prop = self.core.model.prop(full)
        if prop is None:
            mesg = f'No property named "{full}".'
            raise s_exc.NoSuchProp(mesg=mesg)

        if not isinstance(prop.type, s_types.Array):
            mesg = f'Array synax is invalid on non array type: {prop.type.name}.'
            raise s_exc.BadTypeValu(mesg=mesg)

        cmprvals = prop.type.arraytype.getStorCmprs(cmpr, valu)

        if prop.isform:

            for layr in self.layers:
                genr = layr.liftByPropArray(prop.name, None, cmprvals)
                async for node in self._joinStorGenr(layr, genr):
                    if node.bylayer['ndef'] != layr:
                        continue
                    yield node

            return

        formname = None
        if prop.form is not None:
            formname = prop.form.name

        for layr in self.layers:
            genr = layr.liftByPropArray(formname, prop.name, cmprvals)
            async for node in self._joinStorGenr(layr, genr):
                if node.bylayer['props'].get(prop.name) != layr:
                    continue
                yield node

    async def getNodeAdds(self, form, valu, props, addnode=True):

        async def _getadds(f, p, formnorm, forminfo, doaddnode=True):

            edits = []  # Non-primary prop edits
            topsubedits = []  # Primary prop sub edits

            formsubs = forminfo.get('subs', {})
            for subname, subvalu in formsubs.items():
                p[subname] = subvalu

            for propname, propvalu in p.items():
                subedits: s_layer.NodeEditsT = []

                prop = f.prop(propname)
                if prop is None:
                    continue

                assert prop.type.stortype is not None

                if isinstance(prop.type, s_types.Ndef):
                    ndefname, ndefvalu = propvalu
                    ndefform = self.core.model.form(ndefname)
                    if ndefform is None:
                        raise s_exc.NoSuchForm(name=ndefname)

                    ndefnorm, ndefinfo = ndefform.type.norm(ndefvalu)
                    do_subedit = True
                    if self.buidprefetch:
                        node = await self.getNodeByBuid(s_common.buid((ndefform.name, ndefnorm)))
                        do_subedit = node is None

                    if do_subedit:
                        subedits.extend([x async for x in _getadds(ndefform, {}, ndefnorm, ndefinfo)])

                elif isinstance(prop.type, s_types.Array):
                    arrayform = self.core.model.form(prop.type.arraytype.name)
                    if arrayform is not None:
                        for arrayvalu in propvalu:
                            arraynorm, arrayinfo = arrayform.type.norm(arrayvalu)

                            if self.buidprefetch:
                                node = await self.getNodeByBuid(s_common.buid((arrayform.name, arraynorm)))
                                if node is not None:
                                    continue

                            subedits.extend([x async for x in _getadds(arrayform, {}, arraynorm, arrayinfo)])

                propnorm, typeinfo = prop.type.norm(propvalu)

                propsubs = typeinfo.get('subs')
                if propsubs is not None:
                    for subname, subvalu in propsubs.items():
                        fullname = f'{prop.full}:{subname}'
                        subprop = self.core.model.prop(fullname)
                        if subprop is None:
                            continue

                        assert subprop.type.stortype is not None

                        subnorm, subinfo = subprop.type.norm(subvalu)

                        edits.append((s_layer.EDIT_PROP_SET, (subprop.name, subnorm, None, subprop.type.stortype), ()))

                propform = self.core.model.form(prop.type.name)
                if propform is not None:

                    doedit = True
                    if self.buidprefetch:
                        node = await self.getNodeByBuid(s_common.buid((propform.name, propnorm)))
                        doedit = node is None

                    if doedit:
                        subedits.extend([x async for x in _getadds(propform, {}, propnorm, typeinfo)])

                edit: s_layer.EditT = (s_layer.EDIT_PROP_SET, (propname, propnorm, None, prop.type.stortype), subedits)
                if propname in formsubs:
                    topsubedits.append(edit)
                else:
                    edits.append(edit)

            buid = s_common.buid((f.name, formnorm))

            if doaddnode:
                # Make all the sub edits for the primary property a conditional nodeEdit under a top-level NODE_ADD
                # edit
                if topsubedits:
                    subnodeedits: s_layer.NodeEditsT = [(buid, f.name, topsubedits)]
                else:
                    subnodeedits = ()
                topedit: s_layer.EditT = (s_layer.EDIT_NODE_ADD, (formnorm, f.type.stortype), subnodeedits)
                yield (buid, f.name, [topedit] + edits)
            else:
                yield (buid, f.name, edits)

        if props is None:
            props = {}

        norm, info = form.type.norm(valu)
        return [x async for x in _getadds(form, props, norm, info, doaddnode=addnode)]

    async def applyNodeEdit(self, edit):
        nodes = await self.applyNodeEdits((edit,))
        return nodes[0]

    async def applyNodeEdits(self, edits):
        '''
        Sends edits to the write layer and evaluates the consequences (triggers, node object updates)
        '''
        if self.readonly:
            mesg = 'The snapshot is in read-only mode.'
            raise s_exc.IsReadOnly(mesg=mesg)

        meta = await self.getSnapMeta()

        todo = s_common.todo('storNodeEdits', edits, meta)
        results = await self.core.dyncall(self.wlyr.iden, todo)

        wlyr = self.wlyr
        nodes = []
        callbacks = []
        actualedits = []  # List[Tuple[buid, form, changes]]

        # make a pass through the returned edits, apply the changes to our Nodes()
        # and collect up all the callbacks to fire at once at the end.  It is
        # critical to fire all callbacks after applying all Node() changes.

        for buid, sode, postedits in results:

            cache = {wlyr.iden: sode}

            node = await self._joinStorNode(buid, cache)

            if node is None:
                # We got part of a node but no ndef
                continue

            nodes.append(node)

            if postedits:
                actualedits.append((buid, node.form.name, postedits))

            for edit in postedits:

                etyp, parms, _ = edit

                if etyp == s_layer.EDIT_NODE_ADD:
                    node.bylayer['ndef'] = wlyr
                    callbacks.append((node.form.wasAdded, (node,), {}))
                    callbacks.append((self.view.runNodeAdd, (node,), {}))
                    continue

                if etyp == s_layer.EDIT_NODE_DEL:
                    callbacks.append((node.form.wasDeleted, (node,), {}))
                    callbacks.append((self.view.runNodeDel, (node,), {}))
                    continue

                if etyp == s_layer.EDIT_PROP_SET:

                    (name, valu, oldv, stype) = parms

                    prop = node.form.props.get(name)
                    if prop is None: # pragma: no cover
                        logger.warning(f'applyNodeEdits got EDIT_PROP_SET for bad prop {name} on form {node.form}')
                        continue

                    node.props[name] = valu
                    node.bylayer['props'][name] = wlyr

                    callbacks.append((prop.wasSet, (node, oldv), {}))
                    callbacks.append((self.view.runPropSet, (node, prop, oldv), {}))
                    continue

                if etyp == s_layer.EDIT_PROP_DEL:

                    (name, oldv, stype) = parms

                    prop = node.form.props.get(name)
                    if prop is None: # pragma: no cover
                        logger.warning(f'applyNodeEdits got EDIT_PROP_DEL for bad prop {name} on form {node.form}')
                        continue

                    node.props.pop(name, None)
                    node.bylayer['props'].pop(name, None)

                    callbacks.append((prop.wasDel, (node, oldv), {}))
                    callbacks.append((self.view.runPropSet, (node, prop, oldv), {}))
                    continue

                if etyp == s_layer.EDIT_TAG_SET:

                    (tag, valu, oldv) = parms

                    node.tags[tag] = valu
                    node.bylayer['tags'][tag] = wlyr

                    callbacks.append((self.view.runTagAdd, (node, tag, valu), {}))
                    callbacks.append((self.wlyr.fire, ('tag:add', ), {'tag': tag, 'node': node.iden()}))
                    continue

                if etyp == s_layer.EDIT_TAG_DEL:

                    (tag, oldv) = parms

                    node.tags.pop(tag, None)
                    node.bylayer['tags'].pop(tag, None)

                    callbacks.append((self.view.runTagDel, (node, tag, oldv), {}))
                    callbacks.append((self.wlyr.fire, ('tag:del', ), {'tag': tag, 'node': node.iden()}))
                    continue

                if etyp == s_layer.EDIT_TAGPROP_SET:
                    (tag, prop, valu, oldv, stype) = parms
                    node.tagprops[(tag, prop)] = valu
                    node.bylayer['tags'][(tag, prop)] = wlyr
                    continue

                if etyp == s_layer.EDIT_TAGPROP_DEL:
                    (tag, prop, oldv, stype) = parms
                    node.tagprops.pop((tag, prop), None)
                    node.bylayer['tags'].pop((tag, prop), None)
                    continue

        [await func(*args, **kwargs) for (func, args, kwargs) in callbacks]

        if actualedits:
            providen, provstack = self.core.provstor.stor()
            if providen is not None:
                await self.fire('prov:new', time=meta['time'], user=meta['user'], prov=providen, provstack=provstack)
            await self.fire('node:edits', edits=actualedits)

        return nodes

    async def addNode(self, name, valu, props=None):
        '''
        Add a node by form name and value with optional props.

        Args:
            name (str): The form of node to add.
            valu (obj): The value for the node.
            props (dict): Optional secondary properties for the node.

        Notes:
            If a props dictionary is provided, it may be mutated during node construction.

        Returns:
            s_node.Node: A Node object. It may return None if the snap is unable to add or lift the node.
        '''
        if self.readonly:
            mesg = 'The snapshot is in read-only mode.'
            raise s_exc.IsReadOnly(mesg=mesg)

        form = self.core.model.form(name)
        if form is None:
            raise s_exc.NoSuchForm(name=name)

        if form.isrunt:
            raise s_exc.IsRuntForm(mesg='Cannot make runt nodes.',
                                   form=form.full, prop=valu)

        try:

            if self.buidprefetch:
                norm, info = form.type.norm(valu)
                node = await self.getNodeByBuid(s_common.buid((form.name, norm)))
                if node is not None:
                    # TODO implement node.setNodeProps()
                    if props is not None:
                        for p, v in props.items():
                            await node.set(p, v)
                    return node

            adds = await self.getNodeAdds(form, valu, props=props)

        except asyncio.CancelledError:  # pragma: no cover  TODO:  remove once >= py 3.8 only
            raise

        except Exception as e:
            if not self.strict:
                await self.warn(f'addNode: {e}')
                return None
            raise

        nodes = await self.applyNodeEdits(adds)
        assert len(nodes) >= 1

        # Adds is top-down, so the first node is what we want
        return nodes[0]

    async def addFeedNodes(self, name, items):
        '''
        Call a feed function and return what it returns (typically yields Node()s).

        Args:
            name (str): The name of the feed record type.
            items (list): A list of records of the given feed type.

        Returns:
            (object): The return value from the feed function. Typically Node() generator.

        '''
        func = self.core.getFeedFunc(name)
        if func is None:
            raise s_exc.NoSuchName(name=name)

        logger.info(f'User ({self.user.name}) adding feed data ({name}): {len(items)}')

        genr = func(self, items)
        if not isinstance(genr, types.AsyncGeneratorType):
            if isinstance(genr, types.CoroutineType):
                genr.close()
            mesg = f'feed func returned a {type(genr)}, not an async generator.'
            raise s_exc.BadCtorType(mesg=mesg, name=name)

        async for node in genr:
            yield node

    async def addFeedData(self, name, items):

        func = self.core.getFeedFunc(name)
        if func is None:
            raise s_exc.NoSuchName(name=name)

        logger.info(f'User ({self.user.name}) adding feed data ({name}): {len(items)}')

        retn = func(self, items)

        # If the feed function is an async generator, run it...
        if isinstance(retn, types.AsyncGeneratorType):
            retn = [x async for x in retn]
        elif s_coro.iscoro(retn):
            await retn

    async def addTagNode(self, name):
        '''
        Ensure that the given syn:tag node exists.
        '''
        return await self.tagcache.aget(name)

    async def _addTagNode(self, name):
        return await self.addNode('syn:tag', name)

    async def _raiseOnStrict(self, ctor, mesg, **info):
        if self.strict:
            raise ctor(mesg=mesg, **info)
        return False

    async def addNodes(self, nodedefs):
        '''
        Add/merge nodes in bulk.

        The addNodes API is designed for bulk adds which will
        also set properties, add tags, add edges, and set nodedata to existing nodes.
        Nodes are specified as a list of the following tuples:

            ( (form, valu), {'props':{}, 'tags':{}})

        Args:
            nodedefs (list): A list of nodedef tuples.

        Returns:
            (list): A list of xact messages.
        '''
        for (formname, formvalu), forminfo in nodedefs:
            try:
                props = forminfo.get('props')

                # remove any universal created props...
                if props is not None:
                    props.pop('.created', None)

                oldstrict = self.strict
                self.strict = True
                try:
                    node = await self.addNode(formname, formvalu, props=props)
                    if node is not None:
                        tags = forminfo.get('tags')
                        if tags is not None:
                            for tag, asof in tags.items():
                                await node.addTag(tag, valu=asof)

                        tagprops = forminfo.get('tagprops', {})
                        if tagprops is not None:
                            for tag, props in tagprops.items():
                                for prop, valu in props.items():
                                    try:
                                        await node.setTagProp(tag, prop, valu)
                                    except s_exc.NoSuchTagProp:
                                        mesg = \
                                            f'Tagprop [{prop}] does not exist, cannot set it on [{formname}={formvalu}]'
                                        logger.warning(mesg)
                                        continue

                        nodedata = forminfo.get('nodedata')
                        if nodedata is not None:
                            for name, data in nodedata.items():
                                await node.setData(name, data)

                        edges = forminfo.get('edges')
                        if edges is not None:
                            for n2iden, edgeinfo in edges:
                                await node.addEdge(edgeinfo.get('verb'), n2iden)

                except asyncio.CancelledError:  # pragma: no cover  TODO:  remove once >= py 3.8 only
                    raise

                except Exception as e:
                    if not oldstrict:
                        await self.warn(f'addNodes failed on {formname}, {formvalu}, {forminfo}: {e}')
                        continue
                    raise

                finally:
                    self.strict = oldstrict

                yield node

            except asyncio.CancelledError:  # pragma: no cover  TODO:  remove once >= py 3.8 only
                raise

            except Exception:
                logger.exception(f'Error making node: [{formname}={formvalu}]')

    async def getRuntNodes(self, full, valu=None, cmpr=None):

        todo = s_common.todo('runRuntLift', full, valu, cmpr)
        async for sode in self.core.dyniter('cortex', todo):

            node = s_node.Node(self, sode)
            node.isrunt = True

            yield node

    async def iterNodeEdgesN1(self, buid, verb=None):

        async with await s_spooled.Set.anit(dirn=self.core.dirn) as edgeset:

            for layr in self.layers:

                async for edge in layr.iterNodeEdgesN1(buid, verb=verb):
                    if edge in edgeset:
                        continue

                    await edgeset.add(edge)
                    yield edge

    async def iterNodeEdgesN2(self, buid, verb=None):

        async with await s_spooled.Set.anit(dirn=self.core.dirn) as edgeset:

            for layr in self.layers:

                async for edge in layr.iterNodeEdgesN2(buid, verb=verb):
                    if edge in edgeset:
                        continue

                    await edgeset.add(edge)
                    yield edge

    async def getNodeData(self, buid, name, defv=None):
        '''
        Get nodedata from closest to write layer, no merging involved
        '''
        for layr in reversed(self.layers):
            todo = s_common.todo('getNodeData', buid, name)
            ok, valu = await self.core.dyncall(layr.iden, todo)
            if ok:
                return valu
        return defv

    async def iterNodeData(self, buid):
        '''
        Returns:  Iterable[Tuple[str, Any]]
        '''
        some = False
        for layr in reversed(self.layers):
            todo = s_common.todo('iterNodeData', buid)
            async for item in self.core.dyniter(layr.iden, todo):
                some = True
                yield item
            if some:
                return
