import hashlib
import json
from time import time
from uuid import uuid4
from textwrap import dedent
from flask import Flask,jsonify,request
from urllib.parse import urlparse
import requests

class Blockchain(object):
    def __init__(self):
        self.chain=[]
        self.current_transactions=[]
        self.nodes=set()
        #Create the genesis block
        self.new_block(previous_hash=1,proof=100)

    def register_node(self,address):
        '''
        Add a new node to the list of nodes
        :param address: <str> Address of node. ('http://192.168.1.1:5000')
        :return: None
        '''
        parsed_url=urlparse(address)
        if parsed_url.netloc:
            self.nodes.add(parsed_url.netloc)
        elif parsed_url.path:
            #Accepts an URL without scheme like '192.168.0.5:5000'.
            self.nodes.add(parsed_url.path)
        else:
            raise ValueError('Invalid URL')

    def valid_chain(self,chain):
        '''
        Determine if a given blockchain is valid
        :param chain: <list> A blockchain
        :return: <bool> True if valid, False if not
        '''
        last_block=chain[0]
        current_index=1

        while current_index<len(chain):
            block=chain[current_index]
            print(f'{last_block}')
            print(f'{block}')
            print('\n-------------\n')
            #Check that the hash of the block is correct
            if block['previous_hash']!=self.hash(last_block):
                return False
            #Check that the Proof of Work is coorect
            if not self.valid_proof(last_block['proof'],block['proof'],block['previous_hash']):
                return False
            last_block=block
            current_index+=1
        return True

    def resolve_conflicts(self):
        '''
        Consensus Algorithm, it resolves conflicts by replacing our chain with
        the longest one in the network.
        :return: <bool> True if our chain was replaced, False if not
        '''
        neighbours=self.nodes
        new_chain=None
        #Only looking for chains larger than mine
        max_length=len(self.chain)
        #Grab and verify the chains from all the nodes in the network
        for node in neighbours:
            print(f'{node}/chain')
            response=requests.get(f'{node}/chain') #Error occurs here requests.exceptions.InvalidSchema: No connection adapters were found for '127.0.0.1:5000/chain'
            if response.status_code==200:
                length=response.json()['length']
                chain=response.json()['chain']
                #Check if length is longer and if chain is valid
                if (length>max_length) and (self.valid_chain(chain)):
                    max_length=length
                    new_chain=chain
        #Replace my chain if discovered a new, valid chain longer than mine
        if new_chain:
            self.chain=new_chain
            return True
        return False

    def proof_of_work(self,last_block):
        '''
        Simple Proof of Work Algorithm:
        - Find a number p' such that hash(pp') contains 4 leading zeroes,
            where p is the previous p'
        - p is the previous proof, and p' is the new proof
        :param last_block: <dict> last Block
        :return: <int>
        '''
        proof=0
        last_proof=last_block['proof']
        last_hash=self.hash(last_block)
        while self.valid_proof(last_proof,proof,last_hash) is False:
            proof+=1
        return proof

    @staticmethod
    def valid_proof(last_proof,proof,last_hash):
        '''
        Validates the proof: Does hash(last_proof,proof) contain 4 leading zeroes?
        :param last_proof: <int> previous_proof
        :param proof: <int> Current proof
        :param last:hash: <str> The hash of the previous Block
        :return: <bool> True if correct, False otherwise
        '''
        guess=f'{last_proof}{proof}{last_hash}'.encode()
        guess_hash=hashlib.sha256(guess).hexdigest()
        return guess_hash[:4]=='0000'

    def new_block(self,proof,previous_hash=None):
        '''
        Create a new Block in the Blockchain
        :param proof: <int> The proof given by the Proof of Work algorithm
        :param previous_hash: (Optional) <str> Hash of the previous Block
        :return: <dict> New Block
        '''
        block={
            'index':len(self.chain)+1,
            'timestamp':time(),
            'transactions':self.current_transactions,
            'proof':proof,
            'previous_hash':previous_hash or self.hash(self.chain[-1])
        }
        self.current_transactions=[] #Reset the current list of transactions
        self.chain.append(block)
        return block

    def new_transaction(self,sender,recipient,amount):
        '''
        Creates a new transaction to go into the next mined Block
        :param sender: <str> Address of the Sender
        :param recipient: <str> Address of the Recipient
        :param amount: <int> Amount
        :return: <int> The index of the Block that will hold this transaction
        '''
        self.current_transactions.append({
            'sender':sender,
            'recipient':recipient,
            'amount':amount
        })
        return self.last_block['index']+1

    @staticmethod
    def hash(block):
        '''
        Creates a SHA-256 hash of a Block
        :param block: <dict> Block
        :return: <str> The hash
        '''
        block_string=json.dumps(block,sort_keys=True).encode() #Order the dict to avoid inconsistent hashes
        return hashlib.sha256(block_string).hexdigest()

    @property
    def last_block(self):
        '''
        Returns the last block in the chain
        :return: <dict> Last block
        '''
        return self.chain[-1]

app=Flask(__name__) #Instantiate the Node
node_identifier=str(uuid4()).replace('-','') #Generate a globally unique address
blockchain=Blockchain() #Instantiate the Blockchain

@app.route('/mine',methods=['GET'])
def mine():
    #Run the Proof of Work Algorithm to get the next proof
    last_block=blockchain.last_block
    last_proof=last_block['proof']
    proof=blockchain.proof_of_work(last_proof)

    #We receive a reward for finding the proof
    #Sender is '0' to signify that this node has mined a new coin
    blockchain.new_transaction(
        sender='0',
        recipient=node_identifier,
        amount=1
    )

    #Forge the new Block by adding it to the chain
    previous_hash=blockchain.hash(last_block)
    block=blockchain.new_block(proof,previous_hash)

    response={
        'message':'New Block forged',
        'index':block['index'],
        'transactions':block['transactions'],
        'proof':block['proof'],
        'previous_hash':block['previous_hash']
    }
    return jsonify(response),200

@app.route('/transactions/new',methods=['POST'])
def new_transaction():
    values=request.get_json()

    #Check that the required fields are in the POSTed data
    required=['sender','recipient','amount']
    if not all(i in values for i in required):
        return 'Missing values',400

    #Create new transaction
    index=blockchain.new_transaction(values['sender'],values['recipient'],
                                    values['amount'])

    response={'message':f'Transaction will be added to Block {index}'}
    return jsonify(response),201

@app.route('/chain',methods=['GET'])
def full_chain():
    response={
        'chain':blockchain.chain,
        'length':len(blockchain.chain)
    }
    return jsonify(response), 200

@app.route('/nodes/register',methods=['POST'])
def register_nodes():
    values=request.get_json()
    nodes=values.get('nodes')
    if nodes is None:
        return 'Error: Please supply a valid list of nodes',400
    for node in nodes:
        blockchain.register_node(node)
    response={
        'message':'New nodes have been added.',
        'total_nodes':list(blockchain.nodes)
    }
    return jsonify(response),201

@app.route('/nodes/resolve',methods=['GET'])
def consensus():
    replaced=blockchain.resolve_conflicts()
    if replaced:
        response={
            'message':'Our chain was replaced',
            'new_chain':blockchain.chain
        }
    else:
        response={
            'message':'Our chain is authoritative',
            'chain':blockchain.chain
        }
    return jsonify(response),200

@app.route('/')
def home():
    return "Seba's Blockchain API"

if __name__=='__main__':
    app.run(debug=True,host='127.0.0.1',port=5000)
