###############################################################################
#
# Copyright (c) Crossbar.io Technologies GmbH and contributors
#
# 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 https://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 uuid
import binascii

import cbor2
import nacl.secret
import nacl.utils
import nacl.exceptions
import nacl.public

import txaio
from autobahn.twisted.util import sleep
from autobahn.wamp.exception import ApplicationError

import eth_keys
from eth_account import Account

from ._interfaces import IConsumer, IBuyer
from ._util import hl


class SimpleBuyer(object):
    """
    Simple XBR buyer component. This component can be used by a XBR consumer to handle
    XBR buying transactions to buy data keys for services used by the XBR consumer.
    """

    log = txaio.make_logger()

    def __init__(self, buyer_key, max_price):
        """

        :param buyer_key: Consumer delegate (buyer) private Ethereum key.
        :type buyer_key: bytes
        """
        self._pkey = eth_keys.keys.PrivateKey(buyer_key)
        self._acct = Account.privateKeyToAccount(self._pkey)
        self._addr = self._pkey.public_key.to_canonical_address()

        self._max_price = max_price

        # this holds the keys we bought (map: key_id => nacl.secret.SecretBox)
        self._keys = {}
        self._session = None
        self._running = False

        self._receive_key = nacl.public.PrivateKey.generate()

    async def start(self, session, consumer_id):
        """

        :param session:
        :param consumer_id:
        :return:
        """
        self._session = session
        self._running = True

        self.log.info('Start buying from consumer delegate address {address} (public key 0x{public_key}..)',
                      address=self._acct.address,
                      public_key=binascii.b2a_hex(self._pkey.public_key[:10]).decode())

        payment_channel = await session.call('xbr.marketmaker.get_payment_channel', self._addr)

        self.log.info('Delegate current payment channel: {payment_channel}',
                      payment_channel=hl(binascii.b2a_hex(payment_channel['channel']).decode()))

        if not payment_channel:
            raise Exception('no active payment channel found for delegate')
        if payment_channel['state'] != 1:
            raise Exception('payment channel not open')
        if payment_channel['remaining'] == 0:
            raise Exception('payment channel (amount={}) has no balance remaining'.format(payment_channel['remaining']))

        self._channel = payment_channel
        self._balance = payment_channel['remaining']

        return self._balance

    async def unwrap(self, key_id, enc_ser, ciphertext):
        """

        :param key_id:
        :param enc_ser:
        :param ciphertext:
        :return:
        """
        assert(enc_ser == 'cbor')

        # if we don't have the key, buy it!
        if key_id not in self._keys:
            # mark the key as currently being bought already (the location of code here is multi-entrant)
            self._keys[key_id] = False

            # call the market maker to buy the key
            amount = self._max_price

            # FIXME: compute actual kecchak256 based signature
            signature = b'\x00' * 64

            buyer_pubkey = self._receive_key.public_key.encode(encoder=nacl.encoding.RawEncoder)

            # call the market maker to buy the key
            #   -> channel_id, channel_seq, buyer_pubkey, datakey_id, amount, balance, signature
            try:
                receipt = await self._session.call('xbr.marketmaker.buy_key',
                                                   self._addr,
                                                   buyer_pubkey,
                                                   key_id,
                                                   amount,
                                                   signature)
            except Exception as e:
                self._keys[key_id] = e
                raise e

            sealed_key = receipt['sealed_key']

            unseal_box = nacl.public.SealedBox(self._receive_key)
            try:
                key = unseal_box.decrypt(sealed_key)
            except nacl.exceptions.CryptoError as e:
                self._keys[key_id] = e
                raise ApplicationError('xbr.error.decryption_failed', 'could not unseal data encryption key: {}'.format(e))

            # remember the key, so we can use it to actually decrypt application payload data
            self._keys[key_id] = nacl.secret.SecretBox(key)

            self.log.info(
                '{tx_type} key "{key_id}" bought for {amount_paid} [payment_channel="{payment_channel}", remaining={remaining}, inflight={inflight}, buyer_pubkey="{buyer_pubkey}"]',
                tx_type=hl('XBR BUY   ', color='magenta'),
                key_id=hl(uuid.UUID(bytes=key_id)),
                amount_paid=hl(str(receipt['amount_paid']) + ' XBR', color='magenta'),
                payment_channel=hl(binascii.b2a_hex(receipt['payment_channel']).decode()),
                remaining=hl(receipt['remaining']),
                inflight=hl(receipt['inflight']),
                buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()))

        # if the key is already being bought, wait until the one buying path of execution has succeeded and done
        log_counter = 0
        while self._keys[key_id] == False:
            if log_counter % 100:
                self.log.info('Waiting for key "{key_id}" currently being bought ..', key_id=hl(uuid.UUID(bytes=key_id)))
                log_counter += 1
            await sleep(.2)

        # check if the key buying failed and fail the unwrapping in turn
        if isinstance(self._keys[key_id], Exception):
            e = self._keys[key_id]
            raise e

        # now that we have the secret key, decrypt the event application payload
        try:
            message = self._keys[key_id].decrypt(ciphertext)
        except nacl.exceptions.CryptoError as e:
            # Decryption failed. Ciphertext failed verification
            raise ApplicationError('xbr.error.decryption_failed', 'failed to unwrap encrypted data: {}'.format(e))

        try:
            payload = cbor2.loads(message)
        except cbor2.decoder.CBORDecodeError as e:
            # premature end of stream (expected to read 4187 bytes, got 27 instead)
            raise ApplicationError('xbr.error.deserialization_failed', 'failed to deserialize application payload: {}'.format(e))

        return payload


IBuyer.register(SimpleBuyer)
IConsumer.register(SimpleBuyer)
