#!/usr/bin/env python
# -*- coding: utf-8 -*-

from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
import json
from base64 import b64encode, b64decode
from time import time
import pickle
import pprint
import os

class Blockchain:
    def __init__(self):
        self.current_transactions = [] #egy list amiben tároljuk az aktuális saját kimenő tranzakciókat, majd vagy kimentjük ha nem akarjuk kibányászni vagy bányászás elején belerakjuk a többiek tranzakcióit
        self.chain = [] #egy list amiben a blockchaint tároljuk
        f =  open('csuba_priv.pem', 'rb')
        key = RSA.importKey(f.read())
        self.private_key = key #kiolvassuk a saját kulcsunkat a fájlból amit korábban létrehoztunk
        self.public_key= key.publickey() #itt is
        self.my_inputs = [] # [
        #     {'signature': 'valami_aláírás_van_itt_1', 'amount': 10},
        #     {'signature': 'valami_aláírás_van_itt_2', 'amount': 2},
        #     {'signature': 'valami_aláírás_van_itt_3', 'amount': 5},
        #     {'signature': 'valami_aláírás_van_itt_4', 'amount': 4},
        # ] # egy segédlista a saját bejövő tranzakcióimról. Nem muszáj igazából, most csak azért van itt, hogy legyen min gyakorolni


    def sign_data(self, data):
        signer = PKCS1_v1_5.new(self.private_key)
        hash = self.hash(data)
        sign = signer.sign(hash)
        return b64encode(sign).decode()


    def verify(self, data, signature, pub_key):
        signer = PKCS1_v1_5.new(pub_key)
        hash = self.hash(data)
        return signer.verify(hash, b64decode(signature.encode()))


    @staticmethod
    def hash(data):
        hash = SHA256.new()
        encoded_data = json.dumps(data, sort_keys=True).encode()
        hash.update(encoded_data)
        return hash

    def my_balance(self):
        balance = 0
        for tr in self.my_inputs:
            balance += tr['amount']
        return balance

    def new_transaction(self, recipient_public_key, amount):
        f = open(recipient_public_key, 'rb')
        recipient= RSA.importKey(f.read())

        #Keresünk először a tranzakciókból annyit, amennyi elég lesz a kívánt összeg utalásához.
        amount = int(amount)
        sum = 0
        inputs = []
        if(self.my_balance() >= amount): #and recipient != self.public_key): #azért hogy ne utalhass magadnak
            while (sum < amount):
                last_transaction = self.my_inputs.pop()
                inputs.append(last_transaction)
                sum += last_transaction['amount']

            sendback = sum - amount
            transaction = {
                'inputs': inputs,
                'sender': self.public_key.exportKey('PEM').decode(),
                'recipient': recipient.exportKey('PEM').decode(),
                'amount': amount,
                'sendback': sendback,
                'time': time()
            }
            signature = self.sign_data(transaction)
            self.current_transactions.append({'transaction':transaction, 'signature':signature})
        else:
            print("Nincs elég pénzed.")

        return


    def validate_transaction(self, transaction, blockchain = None):
        transaction_content = transaction['transaction']
        signature = transaction['signature']
        sender = RSA.importKey(transaction_content['sender'].encode())

        if not self.verify(transaction_content, signature, sender): #az aláírás tényleg rendben van-e
            return False

        if transaction_content['amount'] != int(transaction_content['amount']) or transaction_content['sendback'] != int(transaction_content['sendback']): #csak egész petákot lehet utalni
            return False

        if transaction_content['amount'] < 0 or transaction_content['sendback'] < 0: #csak pozitív összeget lehet utalni
            return False

        if transaction_content['recipient'] == transaction_content['sender']:
            return False

        valid_transactions = self.get_transactions(sender, blockchain)
        not_valid_transactions = [tr for tr in transaction_content['inputs'] if tr not in valid_transactions]
        if len(not_valid_transactions) != 0: #nincs e olyan tranzakció amit már használt vagy esetleg sose volt az övé
            return False

        if len([d['signature'] for d in transaction_content['inputs']]) != len(set([d['signature'] for d in transaction_content['inputs']])): #ha egy korábbi tranzakciót többször is bele akart rakni a bemenetek közé
            return False

        balance = 0                                 #a felsorolt inputok fedezik-e a költségeket
        for tr in transaction_content['inputs']:
            balance += tr['amount']
        if balance < transaction_content['amount'] + transaction_content['sendback']:
            return False
        return True

    def get_transactions(self, user_pub_key, bc = None): #ezt a függvényt lehet hívni úgy, hogy az aktuálisan érvényesnek gondolt blockchainből gyűjti ki a tranzakciókat
        #illetve úgy is, hogy átadunk neki egy blockchaint és abból szedi ki.
        if bc is None:
            blockchain = self.chain
        else:
            blockchain = bc

        #kigyűjtjük a bejövő tranzakciókat, illetve azokat amiket már elhasznált az adott user
        transactions_in = []
        transactions_out = []
        #végigmegyünk a blockchainen és minden block minden tranzakciójában, ha a user a címzett, akkor bejövő a tranzakciókhoz rakjuk be
        #ha a user a küldő, akkor a visszajárót berakjuk a bejövő tranzakciók közé és a kimenő tranzakció input listáját meg az elköltött tranzakciók listájába rakjuk (extend-del bővíthető a lista)
        for block in blockchain:
            for tr in block['transactions']:
                if tr['transaction']['recipient'] == user_pub_key.exportKey('PEM').decode(): # ne utalj magadnak mert abból gond lehet
                    transactions_in.append({'signature' : tr['signature'], 'amount' : tr['transaction']['amount']})
                elif tr['transaction']['sender'] == user_pub_key.exportKey('PEM').decode():
                    transactions_in.append({'signature' : tr['signature'], 'amount' : tr['transaction']['sendback']})
                    transactions_out.extend(tr['transaction']['inputs'])
        #a még nem elkönyvelt, azaz nem blokkba rakott tranzakciókat is megnézzük, ha itt szerepel mint küldő, akkor azokat az input hivatkozásokat is elköltöttnek nyilvánítjuk.
        for tr in self.current_transactions:
            if tr['transaction']['sender'] == user_pub_key.exportKey('PEM').decode():
                transactions_out.extend(tr['transaction']['inputs'])
        #vesszük a két halmaz különbségét és ezek lesznek a még használható tranzakciók
        available_transactions = [tr for tr in transactions_in if tr not in transactions_out]
        return available_transactions


    def validate_first_transaction(self,transaction): # igazából még a hash-t se kell megnézni szerintem mert ezt a bányás csak belerakja aztán a mininggal igazolja magát a tranzakció
        if len(transaction['transaction']['inputs']) != 0 or transaction['transaction']['amount'] != 10 or transaction['transaction']['sendback'] != 0:
            return False
        return True

    def validate_chain(self, blockchain): #egy teljes blokklánc érvényességét vizsgáló függvény

        last_block = blockchain[0]
        current_index = 1
        #az első blokkban a korábbi hasht nem kell vizsgálni csak a tranzakciók érvényességét és hogy a hash eleje csupa nulla
        #ráadásul az első blokkban nem lehet semmi más tranzakció, csak a bányászfizetés, szóval igazából nem kell vizsgálni, csak az egyetlen bányásztranzakciót, illetve, hogy nincs más mellette
        transactions = last_block['transactions'][:]
        first_tr = transactions.pop(0)
        #ha az első tranzakció az első tranzakciót validáló függvényen nem megy át, illetve a popolás után a tranzakciós lista nem üres, akkor return False

        if not self.validate_first_transaction(first_tr) or len(transactions) > 0:
            return False
        
        #végigmegyünk a maradék blokkokon (mivel ugye párban kell nézni a blokkokat, ezért nem lehet olyan egyszerűen végigiterálni rajta.
        while current_index < len(blockchain):
            block = blockchain[current_index]

            transactions = block['transactions'][:]
            first_tr = transactions.pop(0)
            # ha az első tranzakció az első tranzakciót validáló függvényen nem megy át, akkor return False

            if not self.validate_first_transaction(first_tr):
                return False
            
            #ha a maradék tranzakciók nem mennek át a tranzakció validáláson, akkor return False. fontos, hogy itt a blockchainnek csak azt a darabját adjuk át a validate_transaction függvénynek, amelyik addig a blokkig már érvényes volt.
            #szóval pl. ha a 4. blokkot dolgozzuk fel, akkor csak az első három blokkot adjuk át, hiszen ennek a függvényében kell vizsgálni a tranzakció érvényességét
            for tr in transactions:
                if not self.validate_transaction(tr, blockchain[:current_index]):
                    return False

            # hash vizsgálat hogy tényleg egymásba van-e fűzve a lánc, azaz a last_block hashének hexadecimális ábrázolása egyenlő-e a block-ban lévő previous_hash értékével

            if self.hash(last_block).hexdigest() != block['previous_hash']:
                return False

            # proof of work vizsgálat. Azt kell vizsgálni, hogy az aktuális blokk previous_hash értéke, ami ugye a korábbi block hashe, az átmegy-e a PoW függvényen

            if not self.PoW(self.hash(block).hexdigest()):
                return False
            
            last_block = block
            current_index += 1
        #Az utolsó blockot is megvizsgáljuk még, hogy érvényes e a PoW érték.
        block_hash = self.hash(blockchain[-1]).hexdigest()
        if not self.PoW(block_hash):
            return False

        return True

    def load_chain(self, chain_file):
        chain = self.load_obj("obj", chain_file)
        #pprint.pprint(chain, width=1)
        if self.validate_chain(chain) and len(chain) > len(self.chain):
            self.chain = chain
            #saját tranzakciók frissítése
            self.my_inputs = self.get_transactions(self.public_key)
            #pprint.pprint(self.my_inputs, width=2)

    def mine(self):

        transaction = { #berakunk egy input nélküli tranzakciót a tranzakciók elejére és ebben kapjuk meg a bányászjutalmat
            'inputs': [],
            'sender': self.public_key.exportKey('PEM').decode(),
            'recipient': self.public_key.exportKey('PEM').decode(),
            'amount': 10, #a bányászásért járó jutalom
            'sendback': 0,
            'time': time()
        }
        signature = self.sign_data(transaction)
        transactions_to_block = [{'transaction': transaction, 'signature': signature}]

        while len(self.current_transactions) != 0:#csak érvényes tranzakciót rakjunk a blokkba
            tr = self.current_transactions.pop()
            if self.validate_transaction(tr):
                transactions_to_block.append(tr)

        # for tr in self.current_transactions:
        #     if self.validate_transaction(tr):
        #         transactions_to_block.append(tr)

        if len(self.chain) == 0:
            previous_hash = 1
        else:
            previous_hash = self.hash(self.chain[-1]).hexdigest()

        nonce = 0

        block = {
            'index': len(self.chain),
            'transactions': transactions_to_block,
            'nonce': nonce,
            'previous_hash': previous_hash
        }
        while True:
            block_hash = self.hash(block).hexdigest()
            if self.PoW(block_hash):
                break
            block['nonce'] += 1

        # Reset the current list of transactions
        self.current_transactions = []
        self.my_inputs.extend(self.get_transactions(self.public_key, [block]))
        self.chain.append(block)
        self.save_obj(self.chain, "obj", "blockchain.pkl")
        return block

    @staticmethod
    def PoW(hash):
        return hash[:5] == "00000"


    def export_transactions(self):
        self.save_obj(self.current_transactions, "obj", "transactions.pkl")

    def import_transactions(self):
        for file in os.listdir("transactions"):
            if file.endswith(".pkl"):
                self.current_transactions.extend(self.load_obj("transactions", file))


    @staticmethod
    def save_obj(obj, dir, name):
        with open(os.path.join(dir, name), 'wb') as f:
            pickle.dump(obj, f)

    @staticmethod
    def load_obj(dir, name):
        with open(os.path.join(dir, name ), 'rb') as f:
            return pickle.load(f)


# for i in range(3):
#     bc.mine()
# bc.mine()
# bc.new_transaction('valaki_mas_PUBLIC.pem',8)
# bc.mine()
# pprint.pprint(bc.chain, width=1)
# print(bc.my_balance())

#pprint.pprint(bc.chain, width=1)


bc = Blockchain()
bc.load_chain("blockchain.pkl")
bc.import_transactions()

print(bc.my_balance())
#bc.mine();
#bc.mine();

bc.new_transaction("bartha_barnabas.pem", 15)
bc.mine()
print(bc.my_balance())

#bc.mine()
#bc.new_transaction('valaki_mas_PUBLIC.pem',8)
#bc.new_transaction('valaki_mas2_PUBLIC.pem',12)
#pprint.pprint(bc.chain, width=1)



