"""
Copyright 2017-2018 Government of Canada - Public Services and Procurement Canada - buyandsell.gc.ca

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""


import asyncio
import json
import logging

from os import makedirs
from os.path import basename, expanduser, isfile, join
from time import time
from typing import Set, Union

from indy import anoncreds, blob_storage, ledger
from indy.error import IndyError, ErrorCode
from von_agent.cache import CRED_DEF_CACHE, REVO_CACHE, RevoCacheEntry, SCHEMA_CACHE
from von_agent.codec import cred_attr_value
from von_agent.error import (
    AbsentCredDef,
    AbsentInterval,
    AbsentLinkSecret,
    AbsentRevReg,
    AbsentSchema,
    AbsentTails,
    BadLedgerTxn,
    BadRevocation,
    BadRevStateTime,
    CredentialFocus,
    CorruptTails,
    CorruptWallet)
from von_agent.nodepool import NodePool
from von_agent.tails import Tails
from von_agent.util import (
    cred_def_id,
    cred_def_id2seq_no,
    prune_creds_json,
    rev_reg_id,
    rev_reg_id2cred_def_id__tag,
    schema_id,
    SchemaKey,
    schema_key)
from von_agent.wallet import Wallet


class _BaseAgent:
    """
    Base class for common agent functionality. A VON agent has a wallet and a
    node pool.  It has a role and a cryptonym, and can interact via indy-sdk
    with the distributed ledger that its node pool operates.
    """

    def __init__(self, wallet: Wallet) -> None:
        """
        Initializer for agent. Retain wallet.

        Raise AbsentWallet if wallet is not yet created.

        :param wallet: wallet for agent use
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.__init__: >>> wallet: {}'.format(wallet))

        self._wallet = wallet

        logger.debug('_BaseAgent.__init__: <<<')

    @property
    def pool(self) -> NodePool:
        """
        Accessor for node pool.

        :return: node pool
        """

        return self.wallet.pool

    @property
    def wallet(self) -> Wallet:
        """
        Accessor for wallet.

        :return: wallet
        """

        return self._wallet

    @property
    def did(self) -> str:
        """
        Accessor for agent DID.

        :return: agent DID
        """

        return self.wallet.did

    @property
    def verkey(self) -> str:
        """
        Accessor for agent verification key.

        :return: agent verification key
        """

        return self.wallet.verkey

    async def __aenter__(self) -> '_BaseAgent':
        """
        Context manager entry; open wallet.
        For use in monolithic call opening, using, and closing the agent.

        :return: current object
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.__aenter__: >>>')

        rv = await self.open()

        logger.debug('_BaseAgent.__aenter__: <<<')
        return rv

    async def open(self) -> '_BaseAgent':
        """
        Context manager entry; open wallet.
        For use when keeping agent open across multiple calls.

        :return: current object
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.open: >>>')

        await self.wallet.open()

        logger.debug('_BaseAgent.open: <<<')
        return self

    async def __aexit__(self, exc_type, exc, traceback) -> None:
        """
        Context manager exit. Close wallet.
        For use in monolithic call opening, using, and closing the agent.

        :param exc_type:
        :param exc:
        :param traceback:
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.__aexit__: >>> exc_type: {}, exc: {}, traceback: {}'.format(exc_type, exc, traceback))

        await self.close()
        logger.debug('_BaseAgent.__exit__: <<<')

    async def close(self) -> None:
        """
        Explicit exit. Close wallet.
        For use when keeping agent open across multiple calls.
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.close: >>>')

        await self.wallet.close()

        logger.debug('_BaseAgent.close: <<<')

    async def build_proof_req_json(self, cd_id2spec: dict) -> str:
        """
        Build and return indy-sdk proof request for input attributes and timestamps by cred def id.

        :param cd_id2spec: dict mapping cred def ids to:
            - (optionally) 'attrs': lists of names of attributes of interest (omit for all, empty list or None for none)
            - (optionally) 'minima': (pred) integer lower-bounds of interest (omit, empty list, or None for none)
            - (optionally) 'timestamp': timestamp (epoch seconds) if cred def supports revocation (default now);
            e.g.,

        ::

            {
                'Vx4E82R17q...:3:CL:16': {
                    'attrs': [  # request attrs 'name' and 'favouriteDrink' from this cred def's schema
                        'name',
                        'favouriteDrink'
                    ],
                    'minima': {  # request predicate score>=80 from this cred def
                        'score': 80
                    }
                    'timestamp': 1528116008  # set timestamp for all attrs and preds from this cred def's schema
                },
                'R17v42T4pk...:3:CL:19': None,  # request all attrs, no preds, default timestamps on all attrs
                'e3vc5K168n...:3:CL:23': {},  # request all attrs, no preds, default timestamps on all attrs
                'Z9ccax812j...:3:CL:27': {  # request all attrs, no preds, this timestamp on all attrs
                    'timestamp': 1528116008
                },
                '9cHbp54C8n...:3:CL:37': {  # request no attrs, one pred, specify timestamp on pred
                    'attrs': [],  # or equivalently, 'attrs': None
                    'minima': {
                        'employees': '50'  # nicety: implementation converts to int for caller
                    },
                    'timestamp': 1528116008
                },
                '6caBcmLi33...:3:CL:41': {  # all attrs, one pred, default timestamps to now on attrs & pred
                    'minima': {
                        'regEpoch': 1514782800
                    }
                }
                ...
            }

        :return: indy-sdk proof request json
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.build_proof_req_json: >>> cd_id2spec: {}'.format(cd_id2spec))

        cd_id2schema = {}
        now = int(time())
        proof_req = {
            'nonce': str(int(time())),
            'name': 'proof_req',
            'version': '0.0',
            'requested_attributes': {},
            'requested_predicates': {}
        }

        for cd_id in cd_id2spec:
            interval = None
            cred_def = json.loads(await self.get_cred_def(cd_id))
            seq_no = cred_def_id2seq_no(cd_id)
            cd_id2schema[cd_id] = json.loads(await self.get_schema(seq_no))
            if 'revocation' in cred_def['value']:
                timestamp = cd_id2spec[cd_id].get('timestamp', now) if cd_id2spec[cd_id] else now  # maybe cd_id: None
                interval = {
                    'from': timestamp,
                    'to': timestamp
                }

            for attr in (cd_id2spec[cd_id].get('attrs', cd_id2schema[cd_id]['attrNames']) or []  # maybe 'attrs': None
                    if cd_id2spec[cd_id] else cd_id2schema[cd_id]['attrNames']):  # maybe cd_id: None or cd_id: {}
                attr_uuid = '{}_{}_uuid'.format(seq_no, attr)
                proof_req['requested_attributes'][attr_uuid] = {
                    'name': attr,
                    'restrictions': [{
                        'cred_def_id': cd_id
                    }]
                }
                if interval:
                    proof_req['requested_attributes'][attr_uuid]['non_revoked'] = interval

            for attr in (cd_id2spec[cd_id].get('minima', {}) or {} if cd_id2spec[cd_id] else {}):
                pred_uuid = '{}_{}_uuid'.format(seq_no, attr)
                try:
                    proof_req['requested_predicates'][pred_uuid] = {
                        'name': attr,
                        'p_type': '>=',
                        'p_value': int(cd_id2spec[cd_id]['minima'][attr]),
                        'restrictions': [{
                            'cred_def_id': cd_id
                        }]
                    }
                except ValueError:
                    logger.info('cannot build predicate on non-int minimum {} for {}'.format(
                        cd_id2spec[cd_id]['minima'][attr],
                        attr))
                    continue  # int conversion failed - reject candidate
                if interval:
                    proof_req['requested_predicates'][pred_uuid]['non_revoked'] = interval

        rv_json = json.dumps(proof_req)
        logger.debug('_BaseAgent.build_proof_req_json: <<< {}'.format(rv_json))
        return rv_json

    async def build_req_creds_json(self, creds: dict, filt: dict = None, filt_dflt_incl: bool = False) -> str:
        """
        Build and return indy-sdk requested credentials json from input indy-sdk creds structure
        through specified filter.

        :param creds: indy-sdk creds structure
        :param filt: filter  mapping cred def ids to:
            - (optionally) 'attr-match': dict mapping attributes to values (omit, empty dict, or None to match all);
            - (optionally) 'minima': (pred) integer lower-bounds of interest (omit, empty dict, or None to match all);
            omit parameter or specify empty dict or None for no filter, matching all; e.g.,

        ::

            {
                'Vx4E82R17q...:3:CL:16': {
                    'attr-match': {
                        'name': 'Alex',
                        'sex': 'M',
                        'favouriteDrink': None
                    },
                    'minima': {  # if both attr-match and minima present, combined conjunctively (i.e., via AND)
                        'favouriteNumber' : 10,
                        'score': 100  # if more than one minimum present, combined conjunctively (i.e., via AND)
                    }
                },
                'R17v42T4pk...:3:CL:19': {
                    'attr-match': {
                        'height': 175,
                        'birthdate': '1975-11-15'  # combined conjunctively (i.e., via AND)
                    }
                },
                'Z9ccax812j...:3:CL:27': {
                    'attr-match': {}  # match all attributes on this cred def
                },
                '9cHbp54C8n...:3:CL:37': {
                    'minima': {  # request all attributes on this cred def, request preds specifying employees>=50
                        'employees' : 50,
                    }
                }
                ...
            }

        :param filt_dflt_incl: whether to request (True) all creds by attribute/predicate
            that filter does not identify by cred def, or (False) to exclude them. Note that
            if the filter is None or {}, this parameter is unnecessary - it applies to a filter,
            not a non-filter.
        :return: indy_sdk requested_credentials json for use in proof creation
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.build_req_creds_json: >>> creds: {}, filt: {}'.format(creds, filt))

        req_creds = {
            'self_attested_attributes': {},
            'requested_attributes': {},
            'requested_predicates': {}
        }

        def _add_cred(cred, uuid, key):
            nonlocal req_creds
            req_creds[key][uuid] = {
                'cred_id': cred['cred_info']['referent'],
                'revealed': True
            }
            if cred.get('interval', None):
                req_creds[key][uuid]['timestamp'] = cred['interval']['to']
            if key == 'requested_attributes':
                req_creds[key][uuid]['revealed'] = True

        if filt:
            for cd_id in filt:
                cred_def = json.loads(await self.get_cred_def(cd_id))
                if not cred_def:
                    logger.warning(
                        '_BaseAgent.build_req_creds_json: ignoring filter criterion, no cred def on {}'.format(cd_id))
                    filt.pop(cd_id)

        for attr_uuid in creds['attrs']:
            for cred in creds['attrs'][attr_uuid]:
                if attr_uuid in req_creds['requested_attributes']:
                    continue
                cred_info = cred['cred_info']
                cred_cd_id = cred_info['cred_def_id']

                if filt:
                    if cred_cd_id not in filt:
                        if filt_dflt_incl:
                            _add_cred(cred, attr_uuid, 'requested_attributes')
                        continue
                    if cred_cd_id in filt and 'attr-match' in (filt[cred_cd_id] or {}):  # maybe filt[cred_cd_id]: None
                        if not {k: str(filt[cred_cd_id].get('attr-match', {})[k])
                                for k in filt[cred_cd_id].get('attr-match', {})}.items() <= cred_info['attrs'].items():
                            continue
                    _add_cred(cred, attr_uuid, 'requested_attributes')
                else:
                    _add_cred(cred, attr_uuid, 'requested_attributes')

        for pred_uuid in creds['predicates']:
            for cred in creds['predicates'][pred_uuid]:
                if pred_uuid in req_creds['requested_predicates']:
                    continue
                cred_info = cred['cred_info']
                cred_cd_id = cred_info['cred_def_id']

                if filt:
                    if cred_cd_id not in filt:
                        if filt_dflt_incl:
                            _add_cred(cred, pred_uuid, 'requested_predicates')
                        continue
                    if cred_cd_id in filt and 'minima' in (filt[cred_cd_id] or {}):  # maybe filt[cred_cd_id]: None
                        minima = filt[cred_cd_id].get('minima', {})
                        try:
                            if any((attr not in cred_info['attrs'])
                                or (int(cred_info['attrs'][attr]) < int(minima[attr]))
                                    for attr in minima):
                                continue
                        except ValueError:
                            continue  # int conversion failed - reject candidate
                    _add_cred(cred, pred_uuid, 'requested_predicates')
                else:
                    _add_cred(cred, pred_uuid, 'requested_predicates')

        rv_json = json.dumps(req_creds)
        logger.debug('_BaseAgent.build_req_creds_json: <<< {}'.format(rv_json))
        return rv_json

    async def get_nym(self, did: str) -> str:
        """
        Get json cryptonym (including current verification key) for input (agent) DID from ledger.

        Raise BadLedgerTxn on failure.

        :param did: DID of cryptonym to fetch
        :return: cryptonym json
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.get_nym: >>> did: {}'.format(did))

        rv = json.dumps({})
        get_nym_req = await ledger.build_get_nym_request(self.did, did)
        resp_json = await self._submit(get_nym_req)

        data_json = (json.loads(resp_json))['result']['data']  # it's double-encoded on the ledger
        if data_json:
            rv = data_json

        logger.debug('_BaseAgent.get_nym: <<< {}'.format(rv))
        return rv

    def role(self) -> str:
        """
        Return the indy-sdk role for an agent in building its nym for the trust anchor to send to the ledger.

        :param agent: agent instance
        :return: role string
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.role: >>>')

        rv = None
        if isinstance(self, (AgentRegistrar, Origin, Issuer)):
            rv = 'TRUST_ANCHOR'

        logger.debug('_BaseAgent.role: <<< {}'.format(rv))
        return rv

    async def _submit(self, req_json: str) -> str:
        """
        Submit (json) request to ledger; return (json) result.

        Raise BadLedgerTxn on failure.

        :param req_json: json of request to sign and submit
        :return: json response
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent._submit: >>> json: {}'.format(req_json))

        rv_json = await ledger.submit_request(self.pool.handle, req_json)
        await asyncio.sleep(0)

        resp = json.loads(rv_json)
        if ('op' in resp) and (resp['op'] in ('REQNACK', 'REJECT')):
            logger.debug(
                '_BaseAgent._submit: <!< ledger rejected request: {}'.format(resp['reason']))
            raise BadLedgerTxn('Ledger rejected transaction request: {}'.format(resp['reason']))

        if 'reason' in resp and 'result' in resp and resp['result'].get('seqNo', None) is None:
            logger.debug('_BaseAgent._submit: <!< response indicates no transaction: {}'.format(resp['reason']))
            raise BadLedgerTxn('Response indicates no transaction: {}'.format(resp['reason']))

        logger.debug('_BaseAgent._submit: <<< {}'.format(rv_json))
        return rv_json

    async def _sign_submit(self, req_json: str) -> str:
        """
        Sign and submit (json) request to ledger; return (json) result.

        Raise CorruptWallet if existing wallet's pool is no longer extant,
        or BadLedgerTxn on any other failure.

        :param req_json: json of request to sign and submit
        :return: json response
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent._sign_submit: >>> json: {}'.format(req_json))

        try:
            rv_json = await ledger.sign_and_submit_request(self.pool.handle, self.wallet.handle, self.did, req_json)
            await asyncio.sleep(0)
        except IndyError as x_indy:
            if x_indy.error_code == ErrorCode.WalletIncompatiblePoolError:
                logger.debug('_BaseAgent._sign_submit: <!< Corrupt wallet {} is not compatible with pool {}'.format(
                    self.wallet.name,
                    self.pool.name))
                raise CorruptWallet(
                    'Corrupt wallet {} is not compatible with pool {}'.format(self.wallet.name, self.pool.name))
            else:
                logger.debug(
                    '_BaseAgent._sign_submit: <!<  cannot sign/submit request for ledger: indy error code {}'.format(
                        self.wallet.name))
                raise BadLedgerTxn('Cannot sign/submit request for ledger: indy error code {}'.format(
                    x_indy.error_code))

        resp = json.loads(rv_json)
        if ('op' in resp) and (resp['op'] in ('REQNACK', 'REJECT')):
            logger.debug('_BaseAgent._sign_submit: ledger rejected request: {}'.format(resp['reason']))
            raise BadLedgerTxn('Ledger rejected transaction request: {}'.format(resp['reason']))

        if 'reason' in resp and 'result' in resp and resp['result'].get('seqNo', None) is None:
            logger.debug('_BaseAgent._sign_submit: <!< response indicates no transaction: {}'.format(
                resp['reason']))
            raise BadLedgerTxn('Response indicates no transaction: {}'.format(resp['reason']))

        logger.debug('_BaseAgent._sign_submit: <<< {}'.format(rv_json))
        return rv_json

    async def _get_rev_reg_def(self, rr_id: str) -> str:
        """
        Get revocation registry definition from ledger by its identifier. Raise AbsentRevReg
        for no such revocation registry, logging any error condition and raising BadLedgerTxn
        on bad request.

        Retrieve the revocation registry definition from the agent's revocation cache if it has it;
        cache it en passant if it does not (and such a revocation registry definition exists on the ledger).

        :param rr_id: (revocation registry) identifier string, of the format
            '<issuer-did>:4:<issuer-did>:3:CL:<schema-seq-no>:CL_ACCUM:<tag>'
        :return: revocation registry definition json as retrieved from ledger
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent._get_rev_reg_def: >>> rr_id: {}'.format(rr_id))

        rv_json = json.dumps({})

        with REVO_CACHE.lock:
            revo_cache_entry = REVO_CACHE.get(rr_id, None)
            rr_def = revo_cache_entry.rev_reg_def if revo_cache_entry else None
            if rr_def:
                logger.info('_BaseAgent._get_rev_reg_def: rev reg def for {} from cache'.format(rr_id))
                rv_json = json.dumps(rr_def)
            else:
                get_rrd_req_json = await ledger.build_get_revoc_reg_def_request(self.did, rr_id)
                resp_json = await self._submit(get_rrd_req_json)
                try:
                    (_, rv_json) = await ledger.parse_get_revoc_reg_def_response(resp_json)
                    rr_def = json.loads(rv_json)
                except IndyError:  # ledger replied, but there is no such rev reg
                    logger.debug('_BaseAgent._get_rev_reg_def: <!< no rev reg exists on {}'.format(rr_id))
                    raise AbsentRevReg('No rev reg exists on {}'.format(rr_id))

                if revo_cache_entry is None:
                    REVO_CACHE[rr_id] = RevoCacheEntry(rr_def, None)
                else:
                    REVO_CACHE[rr_id]._rev_reg_def = rr_def

        logger.debug('_BaseAgent._get_rev_reg_def: <<< {}'.format(rv_json))
        return rv_json

    async def get_cred_def(self, cd_id: str) -> str:
        """
        Get credential definition from ledger by its identifier. Raise AbsentCredDef
        for no such credential definition, logging any error condition and raising
        BadLedgerTxn on bad request.

        Retrieve the credential definition from the agent's credential definition cache if it has it; cache it
        en passant if it does not (and if there is a corresponding credential definition on the ledger).

        :param cd_id: (credential definition) identifier string ('<issuer-did>:3:CL:<schema-seq-no>')
        :return: credential definition json as retrieved from ledger
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.get_cred_def: >>> cd_id: {}'.format(cd_id))

        rv_json = json.dumps({})

        with CRED_DEF_CACHE.lock:
            if cd_id in CRED_DEF_CACHE:
                logger.info('_BaseAgent.get_cred_def: got cred def for {} from cache'.format(cd_id))
                rv_json = json.dumps(CRED_DEF_CACHE[cd_id])
                logger.debug('_BaseAgent.get_cred_def: <<< {}'.format(rv_json))
                return rv_json

            req_json = await ledger.build_get_cred_def_request(self.did, cd_id)
            resp_json = await self._submit(req_json)
            resp = json.loads(resp_json)
            if not ('result' in resp and resp['result'].get('data', None)):
                logger.debug('_BaseAgent.get_cred_def: <!< no cred def exists on {}'.format(cd_id))
                raise AbsentCredDef('No cred def exists on {}'.format(cd_id))
            try:
                (_, rv_json) = await ledger.parse_get_cred_def_response(resp_json)
            except IndyError:  # ledger replied, but there is no such cred def
                logger.debug('_BaseAgent.get_cred_def: <!< no cred def exists on {}'.format(cd_id))
                raise AbsentCredDef('No cred def exists on {}'.format(cd_id))
            CRED_DEF_CACHE[cd_id] = json.loads(rv_json)
            logger.info('_BaseAgent.get_cred_def: got cred def {} from ledger'.format(cd_id))

        logger.debug('_BaseAgent.get_cred_def: <<< {}'.format(rv_json))
        return rv_json

    async def get_schema(self, index: Union[SchemaKey, int, str]) -> str:
        """
        Get schema from ledger by SchemaKey namedtuple (origin DID, name, version),
        sequence number, or schema identifier.

        Raise AbsentSchema for no such credential definition, logging any error condition and raising
        BadLedgerTxn on bad request.

        Retrieve the schema from the agent's schema cache if it has it; cache it
        en passant if it does not (and there is a corresponding schema on the ledger).

        :param index: schema key (origin DID, name, version), sequence number, or schema identifier
        :return: schema json, parsed from ledger
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.get_schema: >>> index: {}'.format(index))

        rv_json = json.dumps({})
        with SCHEMA_CACHE.lock:
            if SCHEMA_CACHE.contains(index):
                logger.info('_BaseAgent.get_schema: got schema {} from schema cache'.format(index))
                rv_json = SCHEMA_CACHE[index]
                logger.debug('_BaseAgent.get_schema: <<< {}'.format(rv_json))
                return json.dumps(rv_json)

            if isinstance(index, SchemaKey) or (isinstance(index, str) and ':2:' in index):
                s_id = schema_id(*index) if isinstance(index, SchemaKey) else index
                s_key = schema_key(s_id)
                req_json = await ledger.build_get_schema_request(self.did, s_id)
                resp_json = await self._submit(req_json)
                resp = json.loads(resp_json)
                if not ('result' in resp and resp['result'].get('data', {}).get('attr_names', None)):
                    logger.debug('_BaseAgent.get_schema: <!< no schema exists on {}'.format(index))
                    raise AbsentSchema('No schema exists on {}'.format(index))
                try:
                    (_, rv_json) = await ledger.parse_get_schema_response(resp_json)
                except IndyError:  # ledger replied, but there is no such schema
                    logger.debug('_BaseAgent.get_schema: <!< no schema exists on {}'.format(index))
                    raise AbsentSchema('No schema exists on {}'.format(index))
                SCHEMA_CACHE[s_key] = json.loads(rv_json)  # cache indexes by both txn# and schema key en passant
                logger.info('_BaseAgent.get_schema: got schema {} from ledger'.format(index))

            elif isinstance(index, (int, str)):  # :2: not in index - it's a stringified int txn no
                txn_json = await self.get_txn(int(index))
                txn = json.loads(txn_json)
                if txn.get('type', None) == '101':  # {} for no such txn; 101 marks indy-sdk schema txn type
                    rv_json = await self.get_schema(SchemaKey(
                        txn['identifier'],
                        txn['data']['name'],
                        txn['data']['version']))
                else:
                    logger.info('_BaseAgent.get_schema: no schema at seq #{} on ledger'.format(index))

            else:
                logger.debug('_BaseAgent.get_schema: <!< bad schema index type')
                raise AbsentSchema('Attempt to get schema on ({}) {} , must use schema key or an int'.format(
                    type(index),
                    index))

        logger.debug('_BaseAgent.get_schema: <<< {}'.format(rv_json))
        return rv_json

    async def get_txn(self, txn: int) -> str:
        """
        Find a transaction on the distributed ledger by its sequence number.

        :param txn: transaction number
        :return: json sequence number of transaction, null for no match
        """

        logger = logging.getLogger(__name__)
        logger.debug('_BaseAgent.get_txn: >>> txn: {}'.format(txn))

        rv_json = json.dumps({})
        req_json = await ledger.build_get_txn_request(self.did, txn)
        resp = json.loads(await self._submit(req_json))

        rv_json = json.dumps(resp['result'].get('data', {}))
        logger.debug('_BaseAgent.get_txn: <<< {}'.format(rv_json))
        return rv_json

    def __repr__(self) -> str:
        """
        Return representation for current object.

        :return: representation for current object
        """

        return '{}({})'.format(self.__class__.__name__, self.wallet)


class AgentRegistrar(_BaseAgent):
    """
    Mixin for (trust anchor) agent to register agents onto the distributed ledger
    """

    async def send_nym(self, did: str, verkey: str, alias: str = None, role: str = None) -> None:
        """
        Send input agent's cryptonym (including DID, verification key, plus optional alias and role)
        to the distributed ledger.

        Raise BadLedgerTxn on failure.

        :param did: agent DID to send to ledger
        :param verkey: agent verification key
        :param alias: optional alias
        :param role: agent role on the ledger; specify one of 'TRUSTEE', 'STEWARD', 'TRUST_ANCHOR',
            or else '' to reset role
        """

        logger = logging.getLogger(__name__)
        logger.debug('AgentRegistrar.send_nym: >>> did: {}, verkey: {}, alias: {}, role: {}'.format(
            did,
            verkey,
            alias,
            role or ''))

        req_json = await ledger.build_nym_request(
            self.did,
            did,
            verkey,
            alias,
            role or '')
        await self._sign_submit(req_json)

        logger.debug('AgentRegistrar.send_nym: <<<')


class Origin(_BaseAgent):
    """
    Mixin for agent to send schemata to the distributed ledger
    """

    async def send_schema(self, schema_data_json: str) -> str:
        """
        Send schema to ledger, then retrieve it as written to the ledger and return it.
        If schema already exists on ledger, log error and return schema.

        :param schema_data_json: schema data json with name, version, attribute names; e.g.,

        ::

            {
                'name': 'my-schema',
                'version': '1.234',
                'attr_names': ['favourite_drink', 'height', 'last_visit_date']
            }

        :return: schema json as written to ledger (or existed a priori)
        """

        logger = logging.getLogger(__name__)
        logger.debug('Origin.send_schema: >>> schema_data_json: {}'.format(schema_data_json))

        schema_data = json.loads(schema_data_json)
        s_key = schema_key(schema_id(self.did, schema_data['name'], schema_data['version']))
        with SCHEMA_CACHE.lock:
            try:
                rv_json = await self.get_schema(s_key)
                logger.error('Schema {} version {} already exists on ledger for origin-did {}: not sending'.format(
                    schema_data['name'],
                    schema_data['version'],
                    self.did))
            except AbsentSchema:  # OK - about to create and send it
                (_, schema_json) = await anoncreds.issuer_create_schema(
                    self.did,
                    schema_data['name'],
                    schema_data['version'],
                    json.dumps(schema_data['attr_names']))
                req_json = await ledger.build_schema_request(self.did, schema_json)
                resp_json = await self._sign_submit(req_json)
                resp = json.loads(resp_json)
                resp_result = resp['result']
                rv_json = await self.get_schema(schema_key(schema_id(
                    resp_result['identifier'],
                    resp_result['data']['name'],
                    resp_result['data']['version'])))  # add to cache en passant

        logger.debug('Origin.send_schema: <<< {}'.format(rv_json))
        return rv_json


class Issuer(Origin):
    """
    Mixin for agent acting in role of Issuer. An Issuer creates credential definitions and
    sends them to the ledger, issues credentials, and revokes credentials. Revocation support
    involves the management of tails files and revocation registries.

    For simplicity, the current design calls to make any issuer agent an origin agent.
    """

    def __init__(self, wallet: Wallet) -> None:
        """
        Initializer for Issuer agent. Retain input parameters; do not open wallet nor tails writer.

        :param wallet: wallet for agent use
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer.__init__: >>> wallet: {}'.format(wallet))

        super().__init__(wallet)
        self._dir_tails = join(expanduser('~'), '.indy_client', 'tails')
        makedirs(self._dir_tails, exist_ok=True)

        logger.debug('Issuer.__init__: <<<')

    async def open(self) -> 'Issuer':
        """
        Explicit entry. Perform ancestor opening operations,
        then synchronize revocation registry to tails tree content.

        :return: current object
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer.open: >>>')

        await super().open()
        for path_rr_id in Tails.links(self._dir_tails):
            await self._sync_revoc(basename(path_rr_id))

        logger.debug('Issuer.open: <<<')
        return self

    async def _create_rev_reg(self, rr_id: str, rr_size: int = None) -> None:
        """
        Create revocation registry and new tails file (and association to
        corresponding revocation registry definition via symbolic link) for input
        revocation registry identifier.

        :param rr_id: revocation registry identifier
        :param rr_size: revocation registry size (defaults to 256)
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer._create_rev_reg: >>> rr_id: {}, rr_size: {}'.format(rr_id, rr_size))

        rr_size = rr_size or 256
        (cd_id, tag) = rev_reg_id2cred_def_id__tag(rr_id)

        logger.info('Creating revocation registry (capacity {}) for rev reg id {}'.format(rr_size, rr_id))
        tails_writer_handle = await blob_storage.open_writer(
            'default',
            json.dumps({
                'base_dir': Tails.dir(self._dir_tails, rr_id),
                'uri_pattern': ''
            }))
        apriori = Tails.unlinked(self._dir_tails)
        (rr_id, rrd_json, rre_json) = await anoncreds.issuer_create_and_store_revoc_reg(
            self.wallet.handle,
            self.did,
            'CL_ACCUM',
            tag,
            cd_id,
            json.dumps({
                'max_cred_num': rr_size,
                'issuance_type': 'ISSUANCE_ON_DEMAND'
            }),
            tails_writer_handle)
        delta = Tails.unlinked(self._dir_tails) - apriori
        if len(delta) != 1:
            logger.debug(
                'Issuer._create_rev_reg: <!< Could not create tails file for rev reg id: {}'.format(rr_id))
            raise CorruptTails('Could not create tails file for rev reg id {}'.format(rr_id))
        tails_hash = basename(delta.pop())
        Tails.associate(self._dir_tails, rr_id, tails_hash)

        with REVO_CACHE.lock:
            rrd_req_json = await ledger.build_revoc_reg_def_request(self.did, rrd_json)
            await self._sign_submit(rrd_req_json)
            await self._get_rev_reg_def(rr_id)  # add to cache en passant

        rre_req_json = await ledger.build_revoc_reg_entry_request(self.did, rr_id, 'CL_ACCUM', rre_json)
        await self._sign_submit(rre_req_json)

        logger.debug('Issuer._create_rev_reg: <<<')

    async def _sync_revoc(self, rr_id: str, rr_size: int = None) -> None:
        """
        Create revoc registry if need be for input revocation registry identifier;
        open and cache tails file reader.

        :param rr_id: revocation registry identifier
        :param rr_size: if new revocation registry necessary, its size (default as per _create_rev_reg())
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer._sync_revoc: >>> rr_id: {}, rr_size: {}'.format(rr_id, rr_size))

        (cd_id, tag) = rev_reg_id2cred_def_id__tag(rr_id)

        try:
            await self.get_cred_def(cd_id)
        except AbsentCredDef:
            logger.debug(
                'Issuer._sync_revoc: <!< tails tree {} may be for another ledger; no cred def found on {}'.format(
                    self._dir_tails,
                    cd_id))
            raise AbsentCredDef('Tails tree {} may be for another ledger; no cred def found on {}'.format(
                self._dir_tails,
                cd_id))

        with REVO_CACHE.lock:
            revo_cache_entry = REVO_CACHE.get(rr_id, None)
            tails = None if revo_cache_entry is None else revo_cache_entry.tails
            if tails is None:  #  it's a new revocation registry, or not yet set in cache
                try:
                    tails = await Tails(self._dir_tails, cd_id, tag).open()
                except AbsentTails:
                    await self._create_rev_reg(rr_id, rr_size)   # it's a new revocation registry
                    tails = await Tails(self._dir_tails, cd_id, tag).open()  # symlink should exist now

                if revo_cache_entry is None:
                    REVO_CACHE[rr_id] = RevoCacheEntry(None, tails)
                else:
                    REVO_CACHE[rr_id]._tails = tails

        logger.debug('Issuer._sync_revoc: <<<')

    def path_tails(self, rr_id: str) -> str:
        """
        Return path to tails file for input revocation registry identifier.

        :param rr_id: revocation registry identifier of interest
        :return: path to tails file for input revocation registry identifier
        """

        return Tails.linked(self._dir_tails, rr_id)

    async def send_cred_def(self, s_id: str, revocation: bool = True, rr_size: int = None) -> str:
        """
        Create a credential definition as Issuer, store it in its wallet, and send it to the ledger.

        Raise CorruptWallet for wallet not pertaining to current ledger, BadLedgerTxn on failure
        to send credential definition to ledger if need be, or IndyError for any other failure
        to create and store credential definition in wallet.

        :param s_id: schema identifier
        :param revocation: whether to support revocation for cred def
        :param rr_size: size of initial revocation registry (default as per _create_rev_reg()), if revocation supported
        :return: json credential definition as it appears on ledger
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer.send_cred_def: >>> s_id: {}, revocation: {}, rr_size: {}'.format(
            s_id,
            revocation,
            rr_size))

        rv_json = json.dumps({})
        schema_json = await self.get_schema(schema_key(s_id))
        schema = json.loads(schema_json)

        cd_id = cred_def_id(self.did, schema['seqNo'])
        private_key_ok = True
        with CRED_DEF_CACHE.lock:
            try:
                rv_json = await self.get_cred_def(cd_id)
                logger.info(
                    'Cred def on schema {} version {} already exists on ledger; Issuer {} not sending another'.format(
                        schema['name'],
                        schema['version'],
                        self.wallet.name))
            except AbsentCredDef:
                pass  # OK - about to create, store, and send it

            try:
                (_, cred_def_json) = await anoncreds.issuer_create_and_store_credential_def(
                    self.wallet.handle,
                    self.did,  # issuer DID
                    schema_json,
                    'moot-tag',  # indy-sdk ignores under current indy-sdk revocation model: tags rev regs instead
                    'CL',
                    json.dumps({'support_revocation': revocation}))
                if json.loads(rv_json):
                    private_key_ok = False
                    logger.warning(
                        'New cred def on {} in wallet shadows existing one on ledger: private key not usable'.format(
                            cd_id))  # carry on though, this agent may have other roles so public key is good enough
            except IndyError as x_indy:
                if x_indy.error_code == ErrorCode.AnoncredsCredDefAlreadyExistsError:
                    if json.loads(rv_json):
                        logger.info('Issuer wallet {} reusing existing cred def on schema {} version {}'.format(
                            self.wallet.name,
                            schema['name'],
                            schema['version']))
                    else:
                        logger.debug('Issuer.send_cred_def: <!< corrupt wallet {}'.format(self.wallet.name))
                        raise CorruptWallet(
                            'Corrupt Issuer wallet {} has cred def on schema {} version {} not on ledger'.format(
                                self.wallet.name,
                                schema['name'],
                                schema['version']))
                else:
                    logger.debug(
                        'Issuer.send_cred_def: <!< cannot store cred def in wallet {}: indy error code {}'.format(
                            self.wallet.name,
                            x_indy.error_code))
                    raise

            if not json.loads(rv_json):  # checking the ledger returned no cred def: send it
                req_json = await ledger.build_cred_def_request(self.did, cred_def_json)
                await self._sign_submit(req_json)
                rv_json = await self.get_cred_def(cd_id)  # pick up from ledger and parse; add to cache

                if revocation:
                    await self._sync_revoc(rev_reg_id(cd_id, 0), rr_size)  # create new rev reg, tails file for tag 0

        if revocation and private_key_ok:
            for tag in [str(t) for t in range(int(Tails.next_tag(self._dir_tails, cd_id)[0]))]:  # '0' to str(next-1)
                await self._sync_revoc(rev_reg_id(cd_id, tag), rr_size if tag == 0 else None)

        logger.debug('Issuer.send_cred_def: <<< {}'.format(rv_json))
        return rv_json

    async def create_cred_offer(self, schema_seq_no: int) -> str:
        """
        Create credential offer as Issuer for given schema.

        Raise CorruptWallet if the wallet has no private key for the corresponding credential definition.

        :param schema_seq_no: schema sequence number
        :return: credential offer json for use in storing credentials at HolderProver.
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer.create_cred_offer: >>> schema_seq_no: {}'.format(schema_seq_no))

        rv = None
        cd_id = cred_def_id(self.did, schema_seq_no)
        try:
            rv = await anoncreds.issuer_create_credential_offer(self.wallet.handle, cd_id)
        except IndyError as x_indy:
            if x_indy.error_code == ErrorCode.WalletNotFoundError:
                logger.debug('Issuer.create_cred_offer: <!< did not issue cred definition from wallet {}'.format(
                    self.wallet.name))
                raise CorruptWallet('Cannot create cred offer: did not issue cred definition from wallet {}'.format(
                    self.wallet.name))
            else:
                logger.debug('Issuer.create_cred_offer: <!<  cannot create cred offer, indy error code {}'.format(
                    x_indy.error_code))
                raise

        logger.debug('Issuer.create_cred_offer: <<< {}'.format(rv))
        return rv

    async def create_cred(self, cred_offer_json, cred_req_json: str, cred_attrs: dict) -> (str, str, int):
        """
        Create credential as Issuer out of credential request and dict of key:value (raw, unencoded)
        entries for attributes.

        Return credential json, and if cred def supports revocation, credential revocation identifier
        and revocation registry delta ledger timestamp (epoch seconds).

        If the credential definition supports revocation, and the current revocation registry is full,
        the processing creates a new revocation registry en passant. Depending on the revocation
        registry size (by default starting at 256 and doubling iteratively through 4096), this
        operation may delay credential creation by several seconds.

        :param cred_offer_json: credential offer json as created by Issuer
        :param cred_req_json: credential request json as created by HolderProver
        :param cred_attrs: dict mapping each attribute to its raw value (the operation encodes it); e.g.,

        ::

            {
                'favourite_drink': 'martini',
                'height': 180,
                'last_visit_date': '2017-12-31',
                'weaknesses': None
            }

        :return: newly issued credential json; credential revocation identifier (if cred def supports
            revocation, None otherwise), and ledger timestamp (if cred def supports revocation, None otherwise)
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer.create_cred: >>> cred_offer_json: {}, cred_req_json: {}, cred_attrs: {}'.format(
            cred_offer_json,
            cred_req_json,
            cred_attrs))

        cd_id = json.loads(cred_offer_json)['cred_def_id']
        cred_def = json.loads(await self.get_cred_def(cd_id))  # ensure cred def is in cache

        if 'revocation' in cred_def['value']:
            with REVO_CACHE.lock:
                rr_id = Tails.current_rev_reg_id(self._dir_tails, cd_id)
                tails = REVO_CACHE[rr_id].tails
                assert tails  # at (re)start, at cred def, Issuer sync_revoc() sets this index in revocation cache

                try:
                    (cred_json, cred_revoc_id, rr_delta_json) = await anoncreds.issuer_create_credential(
                        self.wallet.handle,
                        cred_offer_json,
                        cred_req_json,
                        json.dumps({k: cred_attr_value(cred_attrs[k]) for k in cred_attrs}),
                        tails.rr_id,
                        tails.reader_handle)
                    # do not create rr delta frame and append to cached delta frames list: timestamp could lag or skew
                    rre_req_json = await ledger.build_revoc_reg_entry_request(
                        self.did,
                        tails.rr_id,
                        'CL_ACCUM',
                        rr_delta_json)
                    await self._sign_submit(rre_req_json)
                    resp_json = await self._sign_submit(rre_req_json)
                    resp = json.loads(resp_json)
                    rv = (cred_json, cred_revoc_id, resp['result']['txnTime'])

                except IndyError as x_indy:
                    if x_indy.error_code == ErrorCode.AnoncredsRevocationRegistryFullError:
                        (tag, rr_size) = Tails.next_tag(self._dir_tails, cd_id)
                        rr_id = rev_reg_id(cd_id, tag)
                        await self._create_rev_reg(rr_id, rr_size)
                        REVO_CACHE[rr_id]._tails = await Tails(self._dir_tails, cd_id).open()
                        return await self.create_cred(cred_offer_json, cred_req_json, cred_attrs)  # should be ok now
                    else:
                        logger.debug('Issuer.create_cred: <!<  cannot create cred, indy error code {}'.format(
                            x_indy.error_code))
                        raise
        else:
            try:
                (cred_json, _, _) = await anoncreds.issuer_create_credential(
                    self.wallet.handle,
                    cred_offer_json,
                    cred_req_json,
                    json.dumps({k: cred_attr_value(cred_attrs[k]) for k in cred_attrs}),
                    None,
                    None)
                rv = (cred_json, _, _)
            except IndyError as x_indy:
                logger.debug('Issuer.create_cred: <!<  cannot create cred, indy error code {}'.format(
                    x_indy.error_code))
                raise

        logger.debug('Issuer.create_cred: <<< {}'.format(rv))
        return rv

    async def revoke_cred(self, rr_id: str, cr_id) -> int:
        """
        Revoke credential that input revocation registry identifier and
        credential revocation identifier specify.

        Return (epoch seconds) time of revocation.

        Raise AbsentTails if no tails file is available for input
        revocation registry identifier. Raise BadRevocation if issuer cannot
        revoke specified credential for any other reason (e.g., did not issue it,
        already revoked it).

        :param rr_id: revocation registry identifier
        :param cr_id: credential revocation identifier
        :return: time of revocation, in epoch seconds
        """

        logger = logging.getLogger(__name__)
        logger.debug('Issuer.revoke_cred: >>> rr_id: {}, cr_id: {}'.format(rr_id, cr_id))

        tails_reader_handle = (await Tails(
            self._dir_tails,
            *rev_reg_id2cred_def_id__tag(rr_id)).open()).reader_handle
        try:
            rrd_json = await anoncreds.issuer_revoke_credential(
                self.wallet.handle,
                tails_reader_handle,
                rr_id,
                cr_id)
        except IndyError as x_indy:
            logger.debug(
                'Issuer.revoke_cred: <!< Could not revoke revoc reg id {}, cred rev id {}: indy error code {}'.format(
                    rr_id,
                    cr_id,
                    x_indy.error_code))
            raise BadRevocation('Could not revoke revoc reg id {}, cred rev id {}: indy error code {}'.format(
                    rr_id,
                    cr_id,
                    x_indy.error_code))

        rre_req_json = await ledger.build_revoc_reg_entry_request(self.did, rr_id, 'CL_ACCUM', rrd_json)
        resp_json = await self._sign_submit(rre_req_json)
        resp = json.loads(resp_json)

        rv = resp['result']['txnTime']
        logger.debug('Issuer.revoke_cred: <<< {}'.format(rv))
        return rv


class HolderProver(_BaseAgent):
    """
    Mixin for agent acting in the role of w3c Holder and indy-sdk Prover.  A Holder holds
    credentials; a Prover produces proof of credentials. Revocation support requires
    the holder-prover agent to manage tails files.
    """

    def __init__(self, wallet: Wallet) -> None:
        """
        Initializer for HolderProver agent. Retain input parameters; do not open wallet nor tails writer.

        :param wallet: wallet for agent use
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.__init__: >>> wallet: {}'.format(wallet))

        super().__init__(wallet)
        self._link_secret = None

        self._dir_tails = join(expanduser('~'), '.indy_client', 'tails')
        makedirs(self._dir_tails, exist_ok=True)

        logger.debug('HolderProver.__init__: <<<')

    async def _sync_revoc(self, rr_id: str) -> None:
        """
        Pick up tails file reader handle for input revocation registry identifier.  If no symbolic
        link is present, get the revocation registry definition to retrieve its tails file hash,
        then find the tails file and link it. Raise AbsentTails for missing corresponding tails file.

        :param rr_id: revocation registry identifier
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver._sync_revoc: >>> rr_id: {}'.format(rr_id))

        (cd_id, tag) = rev_reg_id2cred_def_id__tag(rr_id)

        if not json.loads(await self.get_cred_def(cd_id)):
            logger.debug(
                'HolderProver._sync_revoc: <!< corrupt tails tree {} may be for another ledger'.format(
                    self._dir_tails))
            raise AbsentCredDef('Corrupt tails tree {} may be for another ledger'.format(self._dir_tails))

        with REVO_CACHE.lock:
            revo_cache_entry = REVO_CACHE.get(rr_id, None)
            tails = revo_cache_entry.tails if revo_cache_entry else None
            if tails is None:  #  it's not yet set in cache
                try:
                    tails = await Tails(self._dir_tails, cd_id, tag).open()
                except AbsentTails:  # get hash from ledger and check for tails file
                    rrdef = json.loads(await self._get_rev_reg_def(rr_id))
                    tails_hash = rrdef['value']['tailsHash']
                    path_tails = join(Tails.dir(self._dir_tails, rr_id), tails_hash)
                    if not isfile(path_tails):
                        logger.debug('HolderProver._sync_revoc: <!< No tails file present at {}'.format(path_tails))
                        raise AbsentTails('No tails file present at {}'.format(path_tails))
                    Tails.associate(self._dir_tails, rr_id, tails_hash)
                    tails = await Tails(self._dir_tails, cd_id, tag).open()  # OK now since tails file present

                if revo_cache_entry is None:
                    REVO_CACHE[rr_id] = RevoCacheEntry(None, tails)
                else:
                    REVO_CACHE[rr_id]._tails = tails

        logger.debug('HolderProver._sync_revoc: <<<')

    async def _build_rr_delta_json(self, rr_id: str, to: int, fro: int = None, fro_delta: dict = None) -> (str, int):
        """
        Build rev reg delta json, potentially starting from existing (earlier) delta.

        Return delta json and its timestamp on the distributed ledger.

        Raise AbsentRevReg for no such revocation registry, or BadRevStateTime for a requested delta to
        a time preceding revocation registry creation.

        :param rr_id: rev reg id
        :param to: time (epoch seconds) of interest; upper-bounds returned timestamp
        :param fro: optional prior time of known delta json
        :param fro_delta: optional known delta as of time fro
        :return: rev reg delta json and ledger timestamp (epoch seconds)
        """

        logger = logging.getLogger(__name__)
        logger.debug('_HolderProver._build_rr_delta_json: >>> rr_id: {}, to: {}, fro: {}, fro_delta: {}'.format(
            rr_id,
            to,
            fro,
            fro_delta))

        rr_delta_json = None
        ledger_timestamp = None

        get_rr_delta_req_json = await ledger.build_get_revoc_reg_delta_request(self.did, rr_id, fro, to)
        resp_json = await self._submit(get_rr_delta_req_json)
        resp = json.loads(resp_json)
        if 'result' in resp and 'data' in resp['result'] and 'value' in resp['result']['data']:
            # delta is to some time at or beyond rev reg creation, carry on
            try:
                (_, rr_delta_json, ledger_timestamp) = await ledger.parse_get_revoc_reg_delta_response(resp_json)
            except IndyError:  # ledger replied, but there is no such rev reg
                logger.debug('_HolderProver._build_rr_delta_json: <!< no rev reg exists on {}'.format(rr_id))
                raise AbsentRevReg('No rev reg exists on {}'.format(rr_id))
        else:
            logger.debug(
                '_HolderProver._build_rr_delta_json: <!< Rev reg {} created after asked-for time {}'.format(rr_id, to))
            raise BadRevStateTime(
                'Rev reg {} created after asked-for time {}'.format(rr_id, to))

        if fro and fro_delta:
            rr_delta_json = await anoncreds.issuer_merge_revocation_registry_deltas(
                json.dumps(fro_delta),
                rr_delta_json)

        rv = (rr_delta_json, ledger_timestamp)
        logger.debug('_HolderProver._build_rr_delta_json: <<< {}'.format(rv))
        return rv


    def dir_tails(self, rr_id: str) -> str:
        """
        Return path to the correct directory for the tails file on input revocation registry identifier.

        :param rr_id: revocation registry identifier of interest
        :return: path to tails dir for input revocation registry identifier
        """

        return Tails.dir(self._dir_tails, rr_id)

    async def open(self) -> 'HolderProver':
        """
        Explicit entry. Perform ancestor opening operations,
        then synchronize revocation registry to tails tree content.

        :return: current object
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.open: >>>')

        await super().open()
        for path_rr_id in Tails.links(self._dir_tails):
            await self._sync_revoc(basename(path_rr_id))

        logger.debug('HolderProver.open: <<<')
        return self

    def rev_regs(self) -> list:
        """
        Return list of revocation registry identifiers for which HolderProver has tails files.

        :return: list of revocation registry identifiers for which HolderProver has tails files
        """

        return [basename(f) for f in Tails.links(self._dir_tails)]

    async def create_link_secret(self, link_secret: str) -> None:
        """
        Create link secret (a.k.a. master secret) used in proofs by HolderProver.

        Raise any IndyError causing failure to set link secret in wallet.

        :param link_secret: label for link secret; indy-sdk uses label to generate link secret
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.create_link_secret: >>> link_secret: {}'.format(link_secret))

        try:
            await anoncreds.prover_create_master_secret(self.wallet.handle, link_secret)
        except IndyError as x_indy:
            if x_indy.error_code == ErrorCode.AnoncredsMasterSecretDuplicateNameError:
                logger.info('HolderProver did not create link secret - it already exists')
            else:
                logger.debug(
                    'HolderProver.create_link_secret: <!<  cannot create link secret {}, indy error code {}'.format(
                        self.wallet.name,
                        x_indy.error_code))
                raise

        self._link_secret = link_secret
        logger.debug('HolderProver.create_link_secret: <<<')

    async def create_cred_req(self, cred_offer_json: str, cd_id: str) -> (str, str):
        """
        Create credential request as HolderProver and store in wallet; return credential json and metadata json.

        Raise AbsentLinkSecret if link secret not set.

        :param cred_offer_json: credential offer json
        :param cd_id: credential definition identifier
        :return: cred request json and corresponding metadata json as created and stored in wallet
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.create_cred_req: >>> cred_offer_json: {}, cd_id: {}'.format(cred_offer_json, cd_id))

        if self._link_secret is None:
            logger.debug('HolderProver.create_cred_req: <!< link secret not set')
            raise AbsentLinkSecret('Link secret is not set')

        # Check that ledger has schema on ledger where cred def expects - in case of pool reset with extant wallet
        cred_def_json = await self.get_cred_def(cd_id)
        schema_seq_no = int(json.loads(cred_def_json)['schemaId'])
        schema_json = await self.get_schema(schema_seq_no)
        schema = json.loads(schema_json)
        if not schema:
            logger.debug(
                'HolderProver.create_cred_req: <!< absent schema@#{}, cred req may be for another ledger'.format(
                    schema_seq_no))
            raise AbsentSchema('Absent schema@#{}, cred req may be for another ledger'.format(schema_seq_no))
        (cred_req_json, cred_req_metadata_json) = await anoncreds.prover_create_credential_req(
            self.wallet.handle,
            self.did,
            cred_offer_json,
            cred_def_json,
            self._link_secret)
        rv = (cred_req_json, cred_req_metadata_json)

        logger.debug('HolderProver.create_cred_req: <<< {}'.format(rv))
        return rv

    async def store_cred(self, cred_json: str, cred_req_metadata_json) -> str:
        """
        Store cred in wallet as HolderProver, return its credential identifier as created in wallet.

        Raise AbsentTails if tails file not available for revocation registry for input credential.

        :param cred_json: credential json as HolderProver created
        :param cred_req_metadata_json: credential request metadata as HolderProver created via create_cred_req()
        :return: credential identifier within wallet
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.store_cred: >>> cred_json: {}, cred_req_metadata_json: {}'.format(
            cred_json,
            cred_req_metadata_json))

        cred = json.loads(cred_json)
        cred_def_json = await self.get_cred_def(cred['cred_def_id'])
        rr_id = cred['rev_reg_id']
        rrdef_json = None
        if rr_id:
            await self._sync_revoc(rr_id)
            rrdef_json = await self._get_rev_reg_def(rr_id)

        rv = await anoncreds.prover_store_credential(
            self.wallet.handle,
            None,  # cred_id, let indy-sdk generate random uuid
            cred_req_metadata_json,
            cred_json,
            cred_def_json,
            rrdef_json)

        logger.debug('HolderProver.store_cred: <<< {}'.format(rv))
        return rv

    async def get_creds_display_coarse(self, filt: dict = None) -> str:
        """
        Return human-readable credentials from wallet by input filter for
        schema identifier and/or credential definition identifier components;
        return all credentials for no filter.

        :param filt: indy-sdk filter for credentials; i.e.,

        ::

            {
                "schema_id": string,  # optional
                "schema_issuer_did": string,  # optional
                "schema_name": string,  # optional
                "schema_version": string,  # optional
                "issuer_did": string,  # optional
                "cred_def_id": string  # optional
            }

        :return: credentials json list; i.e.,

        ::

            [{
                "referent": string,  # credential identifier in the wallet
                "attrs": {
                    "attr1" : {"raw": "value1", "encoded": "value1_as_int" },
                    "attr2" : {"raw": "value2", "encoded": "value2_as_int" },
                    ...
                }
                "schema_id": string,
                "cred_def_id": string,
                "rev_reg_id": Optional<string>,
                "cred_rev_id": Optional<string>
            }]

        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.get_creds_display_coarse: >>> filt: {}'.format(filt))

        rv_json = await anoncreds.prover_get_credentials(self.wallet.handle, json.dumps(filt or {}))
        logger.debug('HolderProver.get_creds_display_coarse: <<< {}'.format(rv_json))
        return rv_json

    async def get_creds(self, proof_req_json: str, filt: dict = None, filt_dflt_incl: bool = False) -> (Set[str], str):
        """
        Get credentials from HolderProver wallet corresponding to proof request and
        filter criteria; return credential identifiers from wallet and credentials json.
        Return empty set and empty production for no such credentials.

        :param proof_req_json: proof request json as Verifier creates; has entries for proof request's
            nonce, name, and version; plus credential's requested attributes, requested predicates. I.e.,

        ::

            {
                'nonce': string,  # indy-sdk makes no semantic specification on this value
                'name': string,  # indy-sdk makes no semantic specification on this value
                'version': numeric-string,  # indy-sdk makes no semantic specification on this value
                'requested_attributes': {
                    '<attr_uuid>': {  # aka attr_referent, a proof-request local identifier
                        'name': string,  # attribute name (matches case- and space-insensitively)
                        'restrictions' [  # optional
                            {
                                "schema_id": string,  # optional
                                "schema_issuer_did": string,  # optional
                                "schema_name": string,  # optional
                                "schema_version": string,  # optional
                                "issuer_did": string,  # optional
                                "cred_def_id": string  # optional
                            },
                            {
                                ...  # if more than one restriction given, combined disjunctively (i.e., via OR)
                            }
                        ],
                        'non_revoked': {  # optional - indy-sdk ignores when getting creds from wallet
                            'from': int,  # optional, epoch seconds
                            'to': int  # optional, epoch seconds
                        }
                    },
                    ...
                },
                'requested_predicates': {
                    '<pred_uuid>': {  # aka predicate_referent, a proof-request local predicate identifier
                        'name': string,  # attribute name (matches case- and space-insensitively)
                        'p_type': '>=',
                        'p_value': int,  # predicate value
                        'restrictions': [  # optional
                            {
                                "schema_id": string,  # optional
                                "schema_issuer_did": string,  # optional
                                "schema_name": string,  # optional
                                "schema_version": string,  # optional
                                "issuer_did": string,  # optional
                                "cred_def_id": string  # optional
                            },
                            {
                                ...  # if more than one restriction given, combined disjunctively (i.e., via OR)
                            }
                        ],
                        'non_revoked': {  # optional - indy-sdk ignores when getting creds from wallet
                            'from': int,  # optional, epoch seconds
                            'to': int  # optional, epoch seconds
                        }
                    },
                    ...
                },
                'non_revoked': {  # optional - indy-sdk ignores when getting creds from wallet
                    'from': Optional<int>,
                    'to': Optional<int>
                }
            }

        :param filt: filter for matching attribute-value pairs and predicates; dict mapping each
            cred def id to dict (specify empty dict or none for no filter, matching all)
            mapping attributes to values to match or compare. E.g.,

        ::

            {
                'Vx4E82R17q...:3:CL:16': {
                    'attr-match': {
                        'name': 'Alex',
                        'sex': 'M',
                        'favouriteDrink': None
                    },
                    'minima': {  # if both attr-match and minima present, combined conjunctively (i.e., via AND)
                        'favouriteNumber' : 10,
                        'score': '100'  # nicety: implementation converts to int for caller
                    },
                },
                'R17v42T4pk...:3:CL:19': {
                    'attr-match': {
                        'height': 175,
                        'birthdate': '1975-11-15'  # combined conjunctively (i.e., via AND)
                    }
                },
                'Z9ccax812j...:3:CL:27': {
                    'attr-match': {}  # match all attributes on this cred def
                }
                ...
            }

        :param filt_dflt_incl: whether to include (True) all credentials from wallet that filter does not
            identify by cred def, or to exclude (False) all such credentials
        :return: tuple with (set of referents, creds json for input proof request);
            empty set and empty production for no such credential
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.get_creds: >>> proof_req_json: {}, filt: {}'.format(proof_req_json, filt))

        if filt is None:
            filt = {}
        rv = None
        creds_json = await anoncreds.prover_get_credentials_for_proof_req(self.wallet.handle, proof_req_json)
        creds = json.loads(creds_json)
        cred_ids = set()

        if filt:
            for cd_id in filt:
                cred_def = json.loads(await self.get_cred_def(cd_id))
                if not cred_def:
                    logger.warning(
                        'HolderProver.get_creds: ignoring filter criterion, no cred def on {}'.format(cd_id))
                    filt.pop(cd_id)

        for uuid, inner_creds in {**creds['attrs'], **creds['predicates']}.items():
            for cred in inner_creds:  # cred is a dict in a list of dicts
                cred_info = cred['cred_info']
                if filt:
                    cred_cd_id = cred_info['cred_def_id']
                    if cred_cd_id not in filt:
                        if filt_dflt_incl:
                            cred_ids.add(cred_info['referent'])
                        continue
                    if 'attr-match' in (filt[cred_cd_id] or {}):  # maybe filt[cred_cd_id]: None
                        if not {k: str(filt[cred_cd_id].get('attr-match', {})[k])
                                for k in filt[cred_cd_id].get('attr-match', {})}.items() <= cred_info['attrs'].items():
                            continue
                    if 'minima' in (filt[cred_cd_id] or {}):  # maybe filt[cred_cd_id]: None
                        minima = filt[cred_cd_id].get('minima', {})
                        try:
                            if any((attr not in cred_info['attrs'])
                                or (int(cred_info['attrs'][attr]) < int(minima[attr]))
                                    for attr in minima):
                                continue
                        except ValueError:
                            continue  # int conversion failed - reject candidate
                    cred_ids.add(cred_info['referent'])
                else:
                    cred_ids.add(cred_info['referent'])

        if filt:
            creds = json.loads(prune_creds_json(creds, cred_ids))

        rv = (cred_ids, json.dumps(creds))
        logger.debug('HolderProver.get_creds: <<< {}'.format(rv))
        return rv

    async def get_creds_by_id(self, proof_req_json: str, cred_ids: set) -> str:
        """
        Get creds structure from HolderProver wallet by credential identifiers.

        :param proof_req_json: proof request as per get_creds() above
        :param cred_ids: set of credential identifiers of interest
        :return: json with cred(s) for input credential identifier(s)
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.get_creds_by_id: >>> proof_req_json: {}, cred_ids: {}'.format(
            proof_req_json,
            cred_ids))

        creds_json = await anoncreds.prover_get_credentials_for_proof_req(self.wallet.handle, proof_req_json)

        # retain only creds of interest: find corresponding referents
        rv_json = prune_creds_json(json.loads(creds_json), cred_ids)
        logger.debug('HolderProver.get_cred_by_referent: <<< {}'.format(rv_json))
        return rv_json

    async def create_proof(self, proof_req: dict, creds: dict, requested_creds: dict) -> str:
        """
        Create proof as HolderProver.

        Raise:
            * AbsentLinkSecret if link secret not set
            * CredentialFocus on attempt to create proof on no creds or multiple creds for a credential definition
            * AbsentTails if missing required tails file
            * BadRevStateTime if a timestamp for a revocation registry state in the proof request
              occurs before revocation registry creation
            * IndyError for any other indy-sdk error.
            * AbsentInterval if creds missing non-revocation interval, but cred def supports revocation

        :param proof_req: proof request as per get_creds() above
        :param creds: credentials to prove
        :param requested_creds: data structure with self-attested attribute info, requested attribute info
            and requested predicate info, assembled from get_creds() and filtered for content of interest. I.e.,

        ::

            {
                'self_attested_attributes': {},
                'requested_attributes': {
                    'attr0_uuid': {
                        'cred_id': string,
                        'timestamp': integer,  # for revocation state
                        'revealed': bool
                    },
                    ...
                },
                'requested_predicates': {
                    'predicate0_uuid': {
                        'cred_id': string,
                        'timestamp': integer  # for revocation state
                    }
                }
            }

        :return: proof json
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.create_proof: >>> proof_req: {}, creds: {}, requested_creds: {}'.format(
                proof_req,
                creds,
                requested_creds))

        if self._link_secret is None:
            logger.debug('HolderProver.create_proof: <!< link secret not set')
            raise AbsentLinkSecret('Link secret is not set')

        x_uuids = [attr_uuid for attr_uuid in creds['attrs'] if len(creds['attrs'][attr_uuid]) != 1]
        if x_uuids:
            logger.debug('HolderProver.create_proof: <!< creds specification out of focus (non-uniqueness)')
            raise CredentialFocus('Proof request requires unique cred per attribute; violators: {}'.format(x_uuids))

        s_id2schema = {}  # schema identifier to schema
        cd_id2cred_def = {}  # credential definition identifier to credential definition
        rr_id2timestamp = {}  # revocation registry of interest to timestamp of interest (or None)
        rr_id2cr_id = {}  # revocation registry of interest to credential revocation identifier
        for referents in {**creds['attrs'], **creds['predicates']}.values():
            interval = referents[0].get('interval', None)
            cred_info = referents[0]['cred_info']
            s_id = cred_info['schema_id']
            if s_id not in s_id2schema:
                s_key = schema_key(s_id)
                schema = json.loads(await self.get_schema(s_key))  # add to cache en passant
                if not schema:
                    logger.debug(
                        'HolderProver.create_proof: <!< absent schema {}, proof req may be for another ledger'
                            .format(s_id))
                    raise AbsentSchema(
                        'Absent schema {}, proof req may be for another ledger'.format(s_id))
                s_id2schema[s_id] = schema

            cd_id = cred_info['cred_def_id']
            if cd_id not in cd_id2cred_def:
                cred_def = json.loads(await self.get_cred_def(cd_id))  # add to cache en passant
                if not cred_def:
                    logger.debug('HolderProver.create_proof: <!< absent cred def for id {}'.format(cd_id))
                    raise AbsentCredDef('Absent cred def for id {}'.format(cd_id))
                cd_id2cred_def[cd_id] = cred_def

            rr_id = cred_info['rev_reg_id']
            if rr_id:
                await self._sync_revoc(rr_id)  # link tails file to its rr_id if it's new
                if interval:
                    if rr_id not in rr_id2timestamp:
                        if interval['to'] > int(time()):
                            logger.debug(
                                'HolderProver.create_proof: <!< interval to {} for rev reg {} is in the future'.format(
                                    interval['to'],
                                    rr_id))
                            raise BadRevStateTime(
                                'Revocation registry {} timestamp {} is in the future'.format(
                                    rr_id,
                                    interval['to']))
                        rr_id2timestamp[rr_id] = interval['to']
                elif 'revocation' in cd_id2cred_def[cd_id]['value']:
                    logger.debug(
                        'HolderProver.create_proof: <!< creds on cred def id {} missing non-revocation interval'.format(
                            cd_id))
                    raise AbsentInterval('Creds on cred def id {} missing non-revocation interval'.format(cd_id))
                if rr_id in rr_id2cr_id:
                    continue
                rr_id2cr_id[rr_id] = cred_info['cred_rev_id']

        rr_id2rev_state = {}  # revocation registry identifier to its state
        with REVO_CACHE.lock:
            for rr_id in rr_id2timestamp:
                revo_cache_entry = REVO_CACHE.get(rr_id, None)
                tails = revo_cache_entry.tails if revo_cache_entry else None
                if tails is None:  # missing tails file
                    logger.debug(
                        'HolderProver.create_proof: <!< missing tails file for rev reg id {}'.format(rr_id))
                    raise AbsentTails('Missing tails file for rev reg id {}'.format(rr_id))
                rr_def_json = await self._get_rev_reg_def(rr_id)
                (rr_delta_json, ledger_timestamp) = await revo_cache_entry.get_delta_json(
                    self._build_rr_delta_json,
                    rr_id2timestamp[rr_id])
                rr_state_json = await anoncreds.create_revocation_state(
                    tails.reader_handle,
                    rr_def_json,
                    rr_delta_json,
                    ledger_timestamp,
                    rr_id2cr_id[rr_id])
                rr_id2rev_state[rr_id] = {
                    rr_id2timestamp[rr_id]: json.loads(rr_state_json)
                }

        rv = await anoncreds.prover_create_proof(
            self.wallet.handle,
            json.dumps(proof_req),
            json.dumps(requested_creds),
            self._link_secret,
            json.dumps(s_id2schema),
            json.dumps(cd_id2cred_def),
            json.dumps(rr_id2rev_state))
        logger.debug('HolderProver.create_proof: <<< {}'.format(rv))
        return rv

    async def reset_wallet(self) -> str:
        """
        Close and delete HolderProver wallet, then create and open a replacement on prior link secret.
        Note that this operation effectively destroys private keys for credential definitions. Its
        intended use is primarily for testing and demonstration.

        Raise AbsentLinkSecret if link secret not set.

        :return: wallet name
        """

        logger = logging.getLogger(__name__)
        logger.debug('HolderProver.reset_wallet: >>>')

        if self._link_secret is None:
            logger.debug('HolderProver.reset_wallet: <!< link secret not set')
            raise AbsentLinkSecret('Link secret is not set')

        seed = self.wallet._seed
        wallet_name = self.wallet.name
        wallet_cfg = self.wallet.cfg
        wallet_xtype = self.wallet.xtype
        wallet_access_creds = self.wallet.access_creds

        await self.wallet.close()
        await self.wallet.remove()
        self._wallet = await Wallet(
            self.pool,
            seed,
            wallet_name,
            wallet_xtype,
            wallet_cfg,
            wallet_access_creds).create()
        await self.wallet.open()

        await self.create_link_secret(self._link_secret)  # carry over link secret to new wallet

        rv = self.wallet.name
        logger.debug('HolderProver.reset_wallet: <<< {}'.format(rv))
        return rv


class Verifier(_BaseAgent):
    """
    Mixin for agent acting in the role of Verifier. Verifier agents verify proofs.
    """

    async def _build_rr_state_json(self, rr_id: str, timestamp: int) -> (str, int):
        """
        Build rev reg state json at a given requested timestamp.

        Return delta json and its transaction time on the distributed ledger,
        with upper bound at input timestamp of interest.
        
        Raise AbsentRevReg if no revocation registry exists on input rev reg id,
        or BadRevStateTime if requested timestamp predates revocation registry creation.

        :param rr_id: rev reg id
        :param timestamp: timestamp of interest (epoch seconds)
        :return: rev reg delta json and ledger timestamp (epoch seconds)
        """

        logger = logging.getLogger(__name__)
        logger.debug('_Verifier._build_rr_state_json: >>> rr_id: {}, timestamp: {}'.format(rr_id, timestamp))

        rr_json = None
        ledger_timestamp = None

        get_rr_req_json = await ledger.build_get_revoc_reg_request(self.did, rr_id, timestamp)
        resp_json = await self._submit(get_rr_req_json)
        resp = json.loads(resp_json)
        if 'result' in resp and 'data' in resp['result'] and 'value' in resp['result']['data']:
            # timestamp at or beyond rev reg creation, carry on
            try:
                (_, rr_json, ledger_timestamp) = await ledger.parse_get_revoc_reg_response(resp_json)
            except IndyError:  # ledger replied, but there is no such rev reg
                logger.debug('Verifier._build_rr_state_json: <!< no rev reg exists on {}'.format(rr_id))
                raise AbsentRevReg('No rev reg exists on {}'.format(rr_id))
        else:
            logger.debug(
                '_Verifier._build_rr_state_json: <!< Rev reg {} created after asked-for time {}'.format(
                    rr_id,
                    timestamp))
            raise BadRevStateTime('Rev reg {} created after asked-for time {}'.format(rr_id, timestamp))

        rv = (rr_json, ledger_timestamp)
        logger.debug('_Verifier._build_rr_state_json: <<< {}'.format(rv))
        return rv

    async def verify_proof(self, proof_req: dict, proof: dict) -> str:
        """
        Verify proof as Verifier. Raise AbsentRevReg if a proof cites a revocation registry
        that does not exist on the distributed ledger.

        :param proof_req: proof request as Verifier creates, as per proof_req_json above
        :param proof: proof as HolderProver creates
        :return: json encoded True if proof is valid; False if not
        """

        logger = logging.getLogger(__name__)
        logger.debug('Verifier.verify_proof: >>> proof_req: {}, proof: {}'.format(proof_req, proof))

        s_id2schema = {}
        cd_id2cred_def = {}
        rr_id2rr_def = {}
        rr_id2rr = {}
        proof_ids = proof['identifiers']
        with REVO_CACHE.lock:
            for proof_id in proof_ids:
                # schema
                s_id = proof_id['schema_id']
                if s_id not in s_id2schema:
                    s_key = schema_key(s_id)
                    schema = json.loads(await self.get_schema(s_key))  # add to cache en passant
                    if not schema:
                        logger.debug(
                            'Verifier.verify_proof: <!< absent schema {}, proof req may be for another ledger'.format(
                                s_id))
                        raise AbsentSchema(
                            'Absent schema {}, proof req may be for another ledger'.format(s_id))
                    s_id2schema[s_id] = schema

                # cred def
                cd_id = proof_id['cred_def_id']
                if cd_id not in cd_id2cred_def:
                    cred_def = json.loads(await self.get_cred_def(cd_id))  # add to cache en passant
                    if not cred_def:
                        logger.debug('Verifier.verify_proof: <!< absent cred def for id {}'.format(cd_id))
                        raise AbsentCredDef('Absent cred def for id {}'.format(cd_id))
                    cd_id2cred_def[cd_id] = cred_def

                # rev reg def
                rr_id = proof_id['rev_reg_id']
                if not rr_id:
                    continue

                rr_def_json = await self._get_rev_reg_def(rr_id)
                rr_id2rr_def[rr_id] = json.loads(rr_def_json)

                # timestamp
                timestamp = proof_id['timestamp']
                revo_cache_entry = REVO_CACHE.get(rr_id, None)
                (rr_json, _) = await revo_cache_entry.get_state_json(self._build_rr_state_json, timestamp)
                if rr_id not in rr_id2rr:
                    rr_id2rr[rr_id] = {}
                rr_id2rr[rr_id][timestamp] = json.loads(rr_json)

        rv = json.dumps(await anoncreds.verifier_verify_proof(
            json.dumps(proof_req),
            json.dumps(proof),
            json.dumps(s_id2schema),
            json.dumps(cd_id2cred_def),
            json.dumps(rr_id2rr_def),
            json.dumps(rr_id2rr)))

        logger.debug('Verifier.verify_proof: <<< {}'.format(rv))
        return rv
