from security.token_manager import TokenManager
import network_constants
from database.entities.transaction import Transaction
from database.repository import Repository
from messaging.rest_fetcher import RestFetcher

import json
import hashlib
import logging
import requests
requests.packages.urllib3.disable_warnings()

from typing import Dict


LOGGER = logging.getLogger(__name__)

class MessageHandler:
    MSG_NOT_JSON = "Message is not in JSON format and is ignored"
    MSG_NO_TYPE = "Message has no type field and is ignored"
    MSG_NOT_PROCESSED = "Message Type could not be processed"
    MSG_TRACE_PROCESSED = "Message handled as blockchain-transaction"

    _mongo_repo = None
    _message_sender = None
    _rest_fetcher = None

    def __init__(self, mongo_repo, message_sender, rest_fetcher:RestFetcher):
        self._mongo_repo = mongo_repo
        self._message_sender = message_sender
        self._message_sender.create_message_destination('datahub', 'direct')
        self._rest_fetcher = rest_fetcher

    def handle_generic(self, body):
        
        result = None

        message = None
        try:
            message = json.loads(body)
        except (ValueError, TypeError):
            result = self.MSG_NOT_JSON
            LOGGER.warning(result)
            LOGGER.info(f"Received message: {body}")
            return result

        if not 'type' in message:
            result = self.MSG_NO_TYPE
            LOGGER.warning(result)
            LOGGER.info(f"Received message: {body}")
            return result
        
        if message['type'] == 'blockchain-transaction':
            self.handle_blockchain_transaction(message['content'])
            result = self.MSG_TRACE_PROCESSED
        else:
            result = self.MSG_NOT_PROCESSED
            LOGGER.warning(result)
            LOGGER.info(f"Received message: {body}")
            

        #LOGGER.info(result) #too much spam
        return result
    
    def _resolve_path(self, data: Dict, path:str) -> Dict:
        '''
        resolves a path without concatenation in a json dictionary

        @params
        data - Required: Dictionary that is the decoded json string
        path - Required: path of multiple keys seperated by "//" and list indizes "[5]"
        '''
        path_pieces = path.split("//")
        value = data

        # resolve all pieces of the path in order
        for i in range(0,len(path_pieces)):
            piece = path_pieces[i]

            # is the current path piece in the form attribute[index]?
            if piece[-1] == "]":
                start = piece.index("[")

                # stem ... attribute name
                # index ... list index
                stem = piece[:start]
                index = int(piece[start+1:-1])

                value = value[stem][index]
            else:
                value = value[piece]
        return value

    def _flatten_transaction(self, transaction: Dict, mappings: Dict):
        '''
        Resolves all paths in [mappings] with the data from [transaction] and creates
        a non-nested flattened element.

        @params
        transaction - Required: dictionary, that is arbitrarily deep nested
        mappings    - Required: contains string->path mappings, describing how the flattened object is built
        '''

        # print("TRANSACTION: "+str(transaction))
        # print("MAPPINGS: "+str(mappings))

        flattened = {}

        # iterate over schema mappings and resolve paths
        for mapping in mappings.keys():
            full_path = mappings[mapping]

            concat_paths = full_path.split("+")
            values = []

            for path in concat_paths:
                values.append(
                    self._resolve_path(transaction, path)
                )

            if len(values) > 1:
                final_value = "".join([str(value) for value in values])
            else:
                final_value = values[0]

            flattened[mapping] = final_value
        
        flattened["UniqueID"] = hashlib.sha256(str(flattened["UniqueID"]).encode("utf-8")).hexdigest()

        return flattened

    def handle_blockchain_transaction(self, transaction_message: Dict):
        '''
        Proccesses a blockchain transaction. The schema gets fetched from the business-logic microservice and a flattened
        version then is created out of it.

        @params
        transaction_message - Required: data of the transaction. Has to contain the key 'ApplicationType' which stores the use-case.
        '''

        # check if there is a use-case in the message
        if "ApplicationType" not in transaction_message:
            LOGGER.error("Transaction has no ApplicationType, storing it under use-case 'unknown'.")
            transaction_message["ApplicationType"] = "unknown"
            self._mongo_repo.add_failed_transaction(transaction_message)
            return

        # check if there is a table in the message
        if "docType" not in transaction_message:
            LOGGER.error("Transaction has no docType, storing it under table 'unknown'.")
            transaction_message["docType"] = "unknown"
            self._mongo_repo.add_failed_transaction(transaction_message)
            return

        transaction_use_case = transaction_message["ApplicationType"]
        transaction_table = transaction_message["docType"]

        try:
            tables = self._rest_fetcher.fetch_schema_information(transaction_use_case)
        except ValueError as e:
            LOGGER.error(f"{e}\nCould not fetch schema, storing it as a failed transaction..")
            self._mongo_repo.add_failed_transaction(transaction_message)
            return

        target_table = None
        # find correct table
        for table in tables:
            if table["name"] == transaction_table:
                target_table = table
                break
        
        # abort if table does not exist.
        if target_table == None:
            LOGGER.error(f"There is no table '{transaction_table}', storing it as a failed transaction.")
            self._mongo_repo.add_failed_transaction(transaction_message)
            return
        
        mappings = table["mappings"]
        try:
            flattened = self._flatten_transaction(transaction_message, mappings)
        except KeyError as e:
            LOGGER.error(f"Failed while flattening with KeyError: {str(e)}, storing it as a failed transaction.")
            self._mongo_repo.add_failed_transaction(transaction_message)
            return

        transaction = Transaction(transaction_use_case, target_table["name"], flattened)

        #check for duplicates
        try:
            references = self._mongo_repo.get_transaction_with_id(transaction.id(),transaction_use_case,transaction_table)
            if references != None:
                LOGGER.info("Found duplicate, storing it as a duplicated transaction.")
                self._mongo_repo.add_duplicated_transaction(transaction)
                return 
        except ValueError as e:
            LOGGER.error(f"{e}, could not insert duplicated node.")
            return

        try:
            self._mongo_repo.add_transaction(transaction)
        except KeyError as e:
            LOGGER.error(f"{e}, ignored {transaction_message}")
            # self._mongo_repo.add_failed_transaction(transaction_message) 
            return


        msg = {
            "type": "new-trace",
            "content": transaction.to_serializable_dict()
        }

        msg_string = json.dumps(msg)
        
        # inform semantic linking microservice
        self._message_sender.send_message('datahub', 'semantic-linking', msg_string)