from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Signature import pkcs1_15
import json
from base64 import b64encode, b64decode
from pprint import pprint
import pickle

class ITKoin:
    def __init__ (self):
        self.pending_transactions = []
        self.unspent_transactions = []
        self.sender_inputs = []
        self.chain = []
        self.chain_filename = 'chain_01.txt'
        self.pending_transactions_filename = 'pending_01.txt'
        self.unspent_outputs_filename = 'unspent_01.txt'
        self.my_privatekey_filename = 'csmkey_03_priv.pem'
        self.unspent_outputs = []
        self.my_unspent_outputs = []
        self.ITKoin_users = ['csmkey_03_id.txt', 'csmkey_04_id.txt'] # Ez egy lista, ahovĂĄ s rĂŠsztvevĹk id-jait tartalmazĂł file-neveket kell felsorolni
        self.initial_csaposhi_offering = 100
        self.my_id = "N2E1YTdiODQ2NWZmZjk0MDM0NTc1NWQ5MzNkNGM1OTE5ZmI2MzQyOGY5MzE0ZjU2ZmI4YmNlNjY3MDU0Y2UwYw===="
        self.my_public_key = ""

    @staticmethod
    def generate_rsa_key(filename):
        rsakey = RSA.generate(2048)
        rsapublickey = rsakey.publickey()
        #pprint(vars(rsakey))
        #pprint(vars(rsapublickey))
        PEMrsakey = rsakey.export_key('PEM')
        #pprint(PEMrsakey)
        PEMrsapublickey = rsapublickey.export_key('PEM')
        #pprint(PEMrsapublickey)
        privatekeyfilename = filename + 'priv.pem'
        f = open(privatekeyfilename, 'wb')
        f.write(PEMrsakey)
        f.close()
        publickeyfilename = filename + 'pub.pem'
        f = open(publickeyfilename, 'wb')
        f.write(PEMrsapublickey)
        f.close()
        return

    def load_key(self, filename):
        privatekeyfilename = filename+'priv.pem'
        privatekeyfileobject = open(privatekeyfilename, 'r')
        privatekeyfilecontent = privatekeyfileobject.read()
        #pprint(privatekeyfilecontent)
        rsakey = RSA.import_key(open(privatekeyfilename,'r').read())
        self.rsakey = rsakey
        #pprint(vars(self.rsakey))
        return

    def load_public_key (self, filename):
        publickeyfilename = filename+'pub.pem'
        publickeyfileobject = open(publickeyfilename, 'r')
        publickeyfilecontent = publickeyfileobject.read()
        #pprint(publickeyfilecontent)
        rsakey = RSA.import_key(open(publickeyfilename,'r').read())
        rsapublickey = rsakey.publickey()
        self.rsapublickey = rsapublickey
        self.my_public_key = open(publickeyfilename, 'r').read()
        #pprint(vars(self.rsapublickey))
        return

    @staticmethod
    def load_id (filename):
        fileobject = open(filename, 'r')
        id = fileobject.read()
        return id

    @staticmethod
    def create_hashobject(data):
        stringdump = json.dumps(data)
        binarydump = stringdump.encode()
        hashobject = SHA256.new(data=binarydump)

        hashhexvalue = hashobject.hexdigest()
        #print(hashhexvalue)
        return hashobject

    @staticmethod
    def create_hashhexvalue (data):
        stringdump = json.dumps(data)
        hashobject = SHA256.new(stringdump.encode())
        return hashobject.hexdigest()

    def create_signature(self, data):
        signatureobject = pkcs1_15.new(self.rsakey)
        hashobject = self.create_hashobject(data)
        signaturevalue = signatureobject.sign(hashobject)
        #print(signaturevalue)
        b64signaturevalue = b64encode(signaturevalue)
        #print(b64signaturevalue)
        #print(b64signaturevalue.decode())
        return b64signaturevalue.decode()

    def verify_signature(self, data, b64signaturevalue, rsapublickey):
        verifyobject = pkcs1_15.new(rsapublickey)
        hashobject = self.create_hashobject(data)
        signaturevalue = b64decode(b64signaturevalue)
        try:
            verifyobject.verify(hashobject, signaturevalue)
            validsignature = True
        except (ValueError, TypeError):
            validsignature = False
        return validsignature

    def check_chain(self):
        last_blocks = self.chain[0]

        current_index = 1
        while current_index < len(self.chain):
            hashobject = self.create_hashobject(last_blocks)
            previous_block_hash = self.create_hashhexvalue(last_blocks["header"])
            if previous_block_hash != self.chain[current_index]["header"]["previous_block_header_hash"]:
                return False
            if self.chain[current_index]["header"]["previous_block_header_hash"][:4] != "0000":
                return False
            if self.chain[current_index]["header"]["transactions_hash"] != self.create_hashhexvalue(self.chain[current_index]["transactions"]):
                return False

            last_blocks = self.chain[current_index]
            current_index += 1

        return True


    @staticmethod
    def save_list(list, filename):
        f = open(filename, 'wb')
        pickle.dump(list, f)
        f.close()
        return

    def load_chain(self):
        fileobject = open(self.chain_filename, 'rb')
        self.chain = pickle.load(fileobject)
        if not self.check_chain():
            self.chain = []
        #pprint(self.chain)
        return

    def check_transaction(self, transaction):
        inputs = transaction["inputs"]
        for input in inputs:
            if not self.verify_signature(input[0], input[3], RSA.import_key(input[4])):
                return False
        return True


    def load_pending_transactions(self):
        fileobject = open(self.pending_transactions_filename, 'rb')
        self.pending_transactions = pickle.load(fileobject)
        #pprint(self.pending_transactions)
        validated_pending_transactions = []
        while len(self.pending_transactions) != 0:
            transaction = self.pending_transactions.pop()
            if self.check_transaction(transaction): # itt validĂĄlni kellene az adott tranzakciĂłt, a nem ĂŠrvĂŠnyeseket eldobja, de nem ĂĄll le
                validated_pending_transactions.append(transaction)
        self.pending_transactions = validated_pending_transactions
        return

    def find_unspent_outputs(self):
        self.unspent_outputs = []
        self.my_unspent_outputs = []
        for transaction in self.chain[0]['transactions']:
            self.unspent_outputs.append([transaction['txid'], 0, self.initial_csaposhi_offering])
            for output in transaction['outputs']:
                if output['recipient']==self.my_id:
                    self.my_unspent_outputs.append([transaction['txid'], 0, output['csaposhi']])
        for block in self.chain[1:]:
            for transaction in block['transactions']:
                for output in transaction['outputs']:
                    self.unspent_outputs.append([transaction['txid'], transaction['outputs'].index(output), output['csaposhi']])
                    if output['recipient'] == self.my_id:
                        self.my_unspent_outputs.append([transaction['txid'], transaction['outputs'].index(output), output['csaposhi']])
                for input in transaction['inputs']:
                    input_data = []
                    input_data.append(input[0])
                    input_data.append(input[1])
                    input_data.append(input[2])
                    self.unspent_outputs.remove(input_data) # minden input biztosan szerepelt a lĂĄnc korĂĄbbi outputjakĂŠnt
                    if input in self.my_unspent_outputs: # a remove() hibĂĄt dob, ha Ăşgy tĂśrlĂźnk a listĂĄbĂłl valamit, hogy nem is volt benne
                        self.my_unspent_outputs.remove(input_data)
        #pprint(self.unspent_outputs)
        #pprint(self.my_unspent_outputs)
        return

    def mine(self):
        transactions_to_block = []
        # itt be kellene tölteni file-ból a pending_transctions-t
        while len(self.pending_transactions) != 0:
            tr = self.pending_transactions.pop()
            # itt validálni kellene az adott tranzakciót
            transactions_to_block.append(tr)

        if len(self.chain) == 0:
            previous_block_hash = 1
        else:
            previous_block = self.chain[-1]["block_header"]
            hashobject = self.create_hashobject(previous_block)# az előző blokkot töltsd be egy hash objektumba a create_hashobject(data) használatával
            previous_block_hash = self.create_hashhexvalue(previous_block)# számold ki az előző blokk lenyomatát hexa értékkel

        nonce = 0

        block_header = {
            'nonce': nonce,
            'previous_block_header_hash': previous_block_hash,
            'transactions_hash' : self.create_hashhexvalue(self.pending_transactions),
        }
        # hozz létre egy új blokkot (block) az alábbi tartalommal: {'index': <blokk sorszáma nullától kezdve>, 'transactions': <tranzakció lista>, 'nonce': <futóindex>, 'previous_block_hash': <előző blokk lenyomata>}
        while True:
            hashobject = self.create_hashobject(block_header)# az adott blokkot töltsd be egy hash objektumba a create_hashobject(data) használatával
            block_hash = self.create_hashhexvalue(block_header)# számold ki az adott blokk lenyomatát hexa értékkel
            if block_hash[:4] == "0000":# ha a lenyomat első két byte-ja hexa 00, kilép a ciklusból
                break
            block_header['nonce'] += 1


        block = {
            'block_header': block_header,
            'transactions': self.pending_transactions
        }
        self.chain.append(block)
        #itt ki kellene file-ba menteni a blokkláncot és kiüríteni a pending_transactions állományt
        self.save_list(self.chain, self.chain_filename)
        self.pending_transactions = []
        pprint(block)
        return

    def generate_first_block(self):
        self.pending_transactions = []
        while len(self.ITKoin_users) > 0:
            recipient_id = self.load_id (self.ITKoin_users.pop()) # elĹveszi a kĂśvetkezĹ id file nevĂŠt ĂŠs beolvassa az id-t
            for tr in self.pending_transactions: # nem szerepelhet kĂŠtszer ugyanaz a recipient, mert akkor a txid azonos lesz
                for op in tr['outputs']:
                    if recipient_id == op['recipient']:
                        pprint ('HIBA: IsmĂŠtlĹdĹ recipient adatok az elsĹ blokk generĂĄlĂĄsakor.')
                        return False
            outputs = [{
                'csaposhi': self.initial_csaposhi_offering,
                'recipient': recipient_id}]
            transaction = {
                'inputs': [],
                'outputs': outputs}
            transaction ['txid'] = self.create_hashhexvalue(transaction) # a tranzakciĂł lenyomata lesz az azonosĂ­tĂłja egyben
            self.pending_transactions.append(transaction)
        #pprint(self.pending_transactions)
        self.mine()
        return

    def new_transaction(self, csaposhi, recipient): # a megadott Ăśsszeg ĂĄtadĂĄsa recipientnek, a maradĂŠk visszautalĂĄsa
        sum = 0
        used_outputs=[]
        while (sum < csaposhi):
            next_output=self.my_unspent_outputs.pop()
            #pprint(next_output)
            used_outputs.append(next_output)
            #pprint(next_output)
            sum += next_output[2] # ebben a listapozĂ­ciĂłban van a hivatkozott outputban kapott Ăśsszeg
            #pprint(sum)
        inputs = used_outputs
        #pprint(inputs)
        for input in inputs: # az inputsban szĂĄndĂŠkosan nincs benne az akkori recipient, mert ezt abbĂłl a tranzakciĂłbĂłl kell kivenni ĂŠs ellenĹrizni
            input.append(self.create_signature(input[0])) # input[3] az alĂĄĂ­rĂĄs ĂŠrtĂŠk base64 kĂłdolĂĄssal
            input.append(self.rsapublickey.export_key().decode('ascii')) # input[4] a publikus kulcsom PEM formĂĄtumban
            #pprint(self.verify_signature(input[0], input[3], RSA.import_key(input[4])))
        outputs = [{
            'csaposhi': csaposhi,
            'recipient': recipient}]
        if sum > csaposhi: # ha van visszajĂĄrĂł, azt visszautaljuk magunknak
            outputs.append({
                'csaposhi': sum-csaposhi,
                'recipient': self.my_id})
        transaction = {
            'inputs': inputs,
            'outputs': outputs}
        transaction['txid'] = self.create_hashhexvalue(transaction) # a tranzakciĂł lenyomata lesz az azonosĂ­tĂłja egyben
        self.pending_transactions.append(transaction)
        #pprint(self.pending_transactions)
        return




coinTest = ITKoin()
#keyFileName = "firstkey"
#coinTest.generate_rsa_key(keyFileName)
#coinTest.load_key(keyFileName)
#coinTest.load_public_key(keyFileName)
#message = 'Hello world'
#sign = coinTest.create_signature(message)
#signBase = b64encode(json.dumps(sign).encode())
#val = coinTest.verify_signature(message, signBase, RSA.import_key(open("firstkeypub.pem",'r').read()).publickey())
#coinTest.mine()

#elso feladat megoldasa
#coinTest.generate_first_block()
#coinTest.load_key("A")
#coinTest.load_public_key("A")
#coinTest.find_unspent_outputs()
#coinTest.new_transaction(5, "NThiODVkMWYyMjYwODM2YmEzMWE4OWEyMmVkMzQ3NWE3N2ZiZmJlOTQ4OGFlMjY4OTQ2OGM3ZGY4ZDNmODMwNw==")
#coinTest.mine()
#coinTest.find_unspent_outputs()
#coinTest.new_transaction(5, "NThiODVkMWYyMjYwODM2YmEzMWE4OWEyMmVkMzQ3NWE3N2ZiZmJlOTQ4OGFlMjY4OTQ2OGM3ZGY4ZDNmODMwNw==")
#coinTest.mine()
#coinTest.save_list(coinTest.chain, coinTest.chain_filename)
#coinTest.load_chain()

#masodik megoldas
#coinTest.generate_first_block()
#coinTest.load_key("A")
#coinTest.load_public_key("A")
#coinTest.find_unspent_outputs()
#coinTest.new_transaction(5, "NThiODVkMWYyMjYwODM2YmEzMWE4OWEyMmVkMzQ3NWE3N2ZiZmJlOTQ4OGFlMjY4OTQ2OGM3ZGY4ZDNmODMwNw==")

#coinTest.my_id = "NThiODVkMWYyMjYwODM2YmEzMWE4OWEyMmVkMzQ3NWE3N2ZiZmJlOTQ4OGFlMjY4OTQ2OGM3ZGY4ZDNmODMwNw=="
#coinTest.load_key("B")
#coinTest.load_public_key("B")
#coinTest.find_unspent_outputs()
#coinTest.new_transaction(5, "N2E1YTdiODQ2NWZmZjk0MDM0NTc1NWQ5MzNkNGM1OTE5ZmI2MzQyOGY5MzE0ZjU2ZmI4YmNlNjY3MDU0Y2UwYw==")

#coinTest.save_list(coinTest.pending_transactions, coinTest.pending_transactions_filename)
#coinTest.load_pending_transactions()

#harmadik feladat
coinTest.generate_first_block()
coinTest.save_list(coinTest.chain, coinTest.chain_filename)
coinTest.load_chain()
print(coinTest.chain)

#negyedik feladat



