#!/usr/bin/env python3

# 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.
# ------------------------------------------------------------------------------
'''
CookieJarTransactionHandler class interfaces for cookiejar Transaction Family.
'''

import traceback
import sys
import hashlib
import logging

from sawtooth_sdk.processor.handler import TransactionHandler
from sawtooth_sdk.processor.exceptions import InvalidTransaction
from sawtooth_sdk.processor.exceptions import InternalError
from sawtooth_sdk.processor.core import TransactionProcessor

# hard-coded for simplicity (otherwise get the URL from the args in main):
#DEFAULT_URL = 'tcp://localhost:4004'
# For Docker:
DEFAULT_URL = 'tcp://validator:4004'

LOGGER = logging.getLogger(__name__)

FAMILY_NAME = "cookiejar"
# TF Prefix is first 6 characters of SHA-512("cookiejar"), a4d219

def _hash(data):
    '''Compute the SHA-512 hash and return the result as hex characters.'''
    return hashlib.sha512(data).hexdigest()

def _get_cookiejar_address(from_key):
    '''
    Return the address of a cookiejar object from the cookiejar TF.

    The address is the first 6 hex characters from the hash SHA-512(TF name),
    plus the result of the hash SHA-512(cookiejar public key).
    '''
    return _hash(FAMILY_NAME.encode('utf-8'))[0:6] + \
                 _hash(from_key.encode('utf-8'))[0:64]


class CookieJarTransactionHandler(TransactionHandler):
    '''
    Transaction Processor class for the cookiejar Transaction Family.

    This TP communicates with the Validator using the accept/get/set functions.
    This implements functions to "bake" or "eat" cookies in a cookie jar.
    '''
    def __init__(self, namespace_prefix):
        '''Initialize the transaction handler class.

           This is setting the "cookiejar" TF namespace prefix.
        '''
        self._namespace_prefix = namespace_prefix

    @property
    def family_name(self):
        '''Return Transaction Family name string.'''
        return FAMILY_NAME

    @property
    def family_versions(self):
        '''Return Transaction Family version string.'''
        return ['1.0']

    @property
    def namespaces(self):
        '''Return Transaction Family namespace 6-character prefix.'''
        return [self._namespace_prefix]

    def apply(self, transaction, context):
        '''This implements the apply function for the TransactionHandler class.

           The apply function does most of the work for this class by
           processing a transaction for the cookiejar transaction family.
        '''

        # Get the payload and extract the cookiejar-specific information.
        # It has already been converted from Base64, but needs deserializing.
        # It was serialized with CSV: action, value
        header = transaction.header
        payload_list = transaction.payload.decode().split(",")
        action = payload_list[0]
        amount = payload_list[1]

        # Get the signer's public key, sent in the header from the client.
        from_key = header.signer_public_key

        # Perform the action.
        LOGGER.info("Action = %s.", action)
        LOGGER.info("Amount = %s.", amount)
        if action == "bake":
            self._make_bake(context, amount, from_key)
        elif action == "eat":
            self._make_eat(context, amount, from_key)
        elif action == "clear":
            self._empty_cookie_jar(context, amount, from_key)
        else:
            LOGGER.info("Unhandled action. Action should be bake or eat")

    @classmethod
    def _make_bake(cls, context, amount, from_key):
        '''Bake (add) "amount" cookies.'''
        cookiejar_address = _get_cookiejar_address(from_key)
        LOGGER.info('Got the key %s and the cookiejar address %s.',
                    from_key, cookiejar_address)
        state_entries = context.get_state([cookiejar_address])
        new_count = 0

        if state_entries == []:
            LOGGER.info('No previous cookies, creating new cookie jar %s.',
                        from_key)
            new_count = int(amount)
        else:
            try:
                count = int(state_entries[0].data)
            except:
                raise InternalError('Failed to load state data')
            new_count = int(amount) + int(count)

        state_data = str(new_count).encode('utf-8')
        addresses = context.set_state({cookiejar_address: state_data})

        if len(addresses) < 1:
            raise InternalError("State Error")
        context.add_event(
            event_type="cookiejar/bake",
            attributes=[("cookies-baked", amount)])

    @classmethod
    def _make_eat(cls, context, amount, from_key):
        '''Eat (subtract) "amount" cookies.'''
        cookiejar_address = _get_cookiejar_address(from_key)
        LOGGER.info('Got the key %s and the cookiejar address %s.',
                    from_key, cookiejar_address)

        state_entries = context.get_state([cookiejar_address])
        new_count = 0

        if state_entries == []:
            LOGGER.info('No cookie jar with the key %s.', from_key)
        else:
            try:
                count = int(state_entries[0].data)
            except:
                raise InternalError('Failed to load state data')
            if count < int(amount):
                raise InvalidTransaction('Not enough cookies to eat. '
                                         'The number should be <= %s.', count)
            else:
                new_count = count - int(amount)

        LOGGER.info('Eating %s cookies out of %d.', amount, count)
        state_data = str(new_count).encode('utf-8')
        addresses = context.set_state(
            {_get_cookiejar_address(from_key): state_data})

        if len(addresses) < 1:
            raise InternalError("State Error")
        context.add_event(
            event_type="cookiejar/eat",
            attributes=[("cookies-ate", amount)])

    @classmethod
    def _empty_cookie_jar(cls, context, amount, from_key):
        cookie_jar_address = _get_cookiejar_address(from_key)
        LOGGER.info("fetched key %s and state address %s", from_key, cookie_jar_address)
        state_entries = context.get_state([cookie_jar_address])
        if state_entries == []:
            LOGGER.info('No cookie jar with the key %s.', from_key)
            return
        else:
            state_data = str(0).encode('utf-8')
            addresses = context.set_state(
                {cookie_jar_address: state_data})

        if len(addresses) < 1:
            raise InternalError("State update Error")
        LOGGER.info("SET global state success")

def main():
    '''Entry-point function for the cookiejar Transaction Processor.'''
    try:
        # Setup logging for this class.
        logging.basicConfig()
        logging.getLogger().setLevel(logging.DEBUG)

        # Register the Transaction Handler and start it.
        processor = TransactionProcessor(url=DEFAULT_URL)
        sw_namespace = _hash(FAMILY_NAME.encode('utf-8'))[0:6]
        handler = CookieJarTransactionHandler(sw_namespace)
        processor.add_handler(handler)
        processor.start()
    except KeyboardInterrupt:
        pass
    except SystemExit as err:
        raise err
    except BaseException as err:
        traceback.print_exc(file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()