# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.    


"""
Module for offchain operations. Connection to resdb nodes not required!
"""

import logging
from functools import singledispatch

from .transaction import Input, Transaction, TransactionLink, _fulfillment_from_details
from .exceptions import KeypairMismatchException

from .exceptions import ResdbException, MissingPrivateKeyError
from .utils import (
    CreateOperation,
    TransferOperation,
    _normalize_operation,
)

logger = logging.getLogger(__name__)


@singledispatch
def _prepare_transaction(
    operation, signers=None, recipients=None, asset=None, metadata=None, inputs=None
):
    raise ResdbException(
        (
            "Unsupported operation: {}. "
            'Only "CREATE" and "TRANSFER" are supported.'.format(operation)
        )
    )


@_prepare_transaction.register(CreateOperation)
def _prepare_create_transaction_dispatcher(operation, **kwargs):
    del kwargs["inputs"]
    return prepare_create_transaction(**kwargs)


@_prepare_transaction.register(TransferOperation)
def _prepare_transfer_transaction_dispatcher(operation, **kwargs):
    del kwargs["signers"]
    return prepare_transfer_transaction(**kwargs)


def prepare_transaction(
    *,
    operation="CREATE",
    signers=None,
    recipients=None,
    asset=None,
    metadata=None,
    inputs=None
) -> dict:
    """! Prepares a transaction payload, ready to be fulfilled. Depending on
    the value of ``operation``, simply dispatches to either
    :func:`~.prepare_create_transaction` or
    :func:`~.prepare_transfer_transaction`.

    @param operation (str): The operation to perform. Must be ``'CREATE'``
            or ``'TRANSFER'``. Case insensitive. Defaults to ``'CREATE'``.
    @param signers (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): 
            One or more public keys representing the issuer(s) of
            the asset being created. Only applies for ``'CREATE'``
            operations. Defaults to ``None``.
    @param recipients (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): 
            One or more public keys representing the new recipients(s)
            of the asset being created or transferred.
            Defaults to ``None``.
    @param asset (:obj:`dict`, optional): 
            The asset to be created orctransferred. 
            MUST be supplied for ``'TRANSFER'`` operations.
            Defaults to ``None``.
    @param metadata (:obj:`dict`, optional): 
            Metadata associated with the
            transaction. Defaults to ``None``.
    @param inputs (:obj:`dict` | :obj:`list` | :obj:`tuple`, optional):
            One or more inputs holding the condition(s) that this
            transaction intends to fulfill. Each input is expected to
            be a :obj:`dict`. Only applies to, and MUST be supplied for,
            ``'TRANSFER'`` operations.

    @return The prepared transaction

    @exception :class:`~.exceptions.ResdbException`: If ``operation`` is
        not ``'CREATE'`` or ``'TRANSFER'``.

    .. important::

        **CREATE operations**

        * ``signers`` MUST be set.
        * ``recipients``, ``asset``, and ``metadata`` MAY be set.
        * If ``asset`` is set, it MUST be in the form of::

            {
                'data': {
                    ...
                }
            }

        * The argument ``inputs`` is ignored.
        * If ``recipients`` is not given, or evaluates to
          ``False``, it will be set equal to ``signers``::

            if not recipients:
                recipients = signers

        **TRANSFER operations**

        * ``recipients``, ``asset``, and ``inputs`` MUST be set.
        * ``asset`` MUST be in the form of::

            {
                'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
            }

        * ``metadata`` MAY be set.
        * The argument ``signers`` is ignored.

    """
    operation = _normalize_operation(operation)
    return _prepare_transaction(
        operation,
        signers=signers,
        recipients=recipients,
        asset=asset,
        metadata=metadata,
        inputs=inputs,
    )


def prepare_create_transaction(*, signers, recipients=None, asset=None, metadata=None):
    """! Prepares a ``"CREATE"`` transaction payload, ready to be
    fulfilled.

    @param signers (:obj:`list` | :obj:`tuple` | :obj:`str`): 
            One or more public keys representing 
            the issuer(s) of the asset being created.
    @param recipients (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): 
            One or more public keys representing 
            the new recipients(s) of the asset being created. Defaults to ``None``.
    @param asset (:obj:`dict`, optional): The asset to be created. Defaults to ``None``.
    @param metadata (:obj:`dict`, optional): Metadata associated with the transaction. Defaults to ``None``.

    @return The prepared ``"CREATE"`` transaction.

    .. important::

        * If ``asset`` is set, it MUST be in the form of::

                {
                    'data': {
                        ...
                    }
                }

        * If ``recipients`` is not given, or evaluates to
          ``False``, it will be set equal to ``signers``::

            if not recipients:
                recipients = signers

    """
    if not isinstance(signers, (list, tuple)):
        signers = [signers]
    # NOTE: Needed for the time being. See
    # https://github.com/bigchaindb/bigchaindb/issues/797
    elif isinstance(signers, tuple):
        signers = list(signers)

    if not recipients:
        recipients = [(signers, 1)]
    elif not isinstance(recipients, (list, tuple)):
        recipients = [([recipients], 1)]
    # NOTE: Needed for the time being. See
    # https://github.com/bigchaindb/bigchaindb/issues/797
    elif isinstance(recipients, tuple):
        recipients = [(list(recipients), 1)]

    transaction = Transaction.create(
        signers,
        recipients,
        metadata=metadata,
        asset=asset["data"] if asset else None,
    )
    return transaction.to_dict()


def prepare_transfer_transaction(*, inputs, recipients, asset, metadata=None):
    """! Prepares a ``"TRANSFER"`` transaction payload, ready to be
    fulfilled.

    @param inputs (:obj:`dict` | :obj:`list` | :obj:`tuple`): 
                One or more inputs holding the condition(s) that this transaction
                intends to fulfill. Each input is expected to be a
                :obj:`dict`.
    @param recipients (:obj:`str` | :obj:`list` | :obj:`tuple`): 
            One or more public keys representing the 
            new recipients(s) of the
            asset being transferred.
    @param asset (:obj:`dict`): A single-key dictionary holding the ``id``
            of the asset being transferred with this transaction.
    @param metadata (:obj:`dict`): Metadata associated with the
            transaction. Defaults to ``None``.

    @return The prepared ``"TRANSFER"`` transaction.

    .. important::

        * ``asset`` MUST be in the form of::

            {
                'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
            }

    Example:

        # .. todo:: Replace this section with docs.

        In case it may not be clear what an input should look like, say
        Alice (public key: ``'3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf'``)
        wishes to transfer an asset over to Bob
        (public key: ``'EcRawy3Y22eAUSS94vLF8BVJi62wbqbD9iSUSUNU9wAA'``).
        Let the asset creation transaction payload be denoted by
        ``tx``::

            # noqa E501
            >>> tx
                {'asset': {'data': {'msg': 'Hello Resdb!'}},
                 'id': '9650055df2539223586d33d273cb8fd05bd6d485b1fef1caf7c8901a49464c87',
                 'inputs': [{'fulfillment': {'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
                                             'type': 'ed25519-sha-256'},
                             'fulfills': None,
                             'owners_before': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}],
                 'metadata': None,
                 'operation': 'CREATE',
                 'outputs': [{'amount': '1',
                              'condition': {'details': {'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
                                                        'type': 'ed25519-sha-256'},
                                            'uri': 'ni:///sha-256;7ApQLsLLQgj5WOUipJg1txojmge68pctwFxvc3iOl54?fpt=ed25519-sha-256&cost=131072'},
                              'public_keys': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}],
                 'version': '2.0'}

        Then, the input may be constructed in this way::

            output_index
            output = tx['transaction']['outputs'][output_index]
            input_ = {
                'fulfillment': output['condition']['details'],
                'input': {
                    'output_index': output_index,
                    'transaction_id': tx['id'],
                },
                'owners_before': output['public_keys'],
            }

        Displaying the input on the prompt would look like::

            >>> input_
            {'fulfillment': {
              'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
              'type': 'ed25519-sha-256'},
             'input': {'output_index': 0,
              'transaction_id': '9650055df2539223586d33d273cb8fd05bd6d485b1fef1caf7c8901a49464c87'},
             'owners_before': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}


        To prepare the transfer:

        >>> prepare_transfer_transaction(
        ...     inputs=input_,
        ...     recipients='EcRawy3Y22eAUSS94vLF8BVJi62wbqbD9iSUSUNU9wAA',
        ...     asset=tx['transaction']['asset'],
        ... )

    """
    if not isinstance(inputs, (list, tuple)):
        inputs = (inputs,)
    if not isinstance(recipients, (list, tuple)):
        recipients = [([recipients], 1)]

    # NOTE: Needed for the time being. See
    # https://github.com/bigchaindb/bigchaindb/issues/797
    if isinstance(recipients, tuple):
        recipients = [(list(recipients), 1)]

    fulfillments = [
        Input(
            _fulfillment_from_details(input_["fulfillment"]),
            input_["owners_before"],
            fulfills=TransactionLink(
                txid=input_["fulfills"]["transaction_id"],
                output=input_["fulfills"]["output_index"],
            ),
        )
        for input_ in inputs
    ]

    transaction = Transaction.transfer(
        fulfillments,
        recipients,
        asset_id=asset["id"],
        metadata=metadata,
    )
    return transaction.to_dict()


def fulfill_transaction(transaction, *, private_keys) -> dict:
    """! Fulfills the given transaction.

    @param transaction The transaction to be fulfilled.
    @param private_keys One or more private keys to be 
            used for fulfilling the transaction.

    @return The fulfilled transaction payload, ready to be sent to a
            ResDB federation.

    @exception :exc:`~.exceptions.MissingPrivateKeyError`: If a private
        key is missing.
    """
    if not isinstance(private_keys, (list, tuple)):
        private_keys = [private_keys]

    # NOTE: Needed for the time being. See
    # https://github.com/bigchaindb/bigchaindb/issues/797
    if isinstance(private_keys, tuple):
        private_keys = list(private_keys)

    transaction_obj = Transaction.from_dict(transaction)
    try:
        signed_transaction = transaction_obj.sign(private_keys)
    except KeypairMismatchException as exc:
        raise MissingPrivateKeyError("A private key is missing!") from exc

    return signed_transaction.to_dict()
