package com.alphawallet.app.util;

import android.text.TextUtils;

import com.alphawallet.app.entity.UnableToResolveENS;
import com.alphawallet.app.entity.tokenscript.TokenscriptFunction;
import com.alphawallet.app.repository.EthereumNetworkBase;
import com.alphawallet.app.repository.TokenRepository;

import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.crypto.Keys;
import org.web3j.crypto.WalletUtils;
import org.web3j.ens.Contracts;
import org.web3j.ens.EnsResolutionException;
import org.web3j.ens.NameHash;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.EthBlock;
import org.web3j.protocol.core.methods.response.EthCall;
import org.web3j.protocol.core.methods.response.EthSyncing;
import org.web3j.protocol.core.methods.response.NetVersion;
import org.web3j.utils.Numeric;

import java.io.InterruptedIOException;
import java.math.BigInteger;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;

import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction;

/**
 * EnsResolver from Web3j adapted for Android Java's BigInteger
 */
public class EnsResolver {

    public static final long DEFAULT_SYNC_THRESHOLD = 1000 * 60 * 3;
    public static final String REVERSE_NAME_SUFFIX = ".addr.reverse";
    public static final String CRYPTO_RESOLVER = "0xD1E5b0FF1287aA9f9A268759062E4Ab08b9Dacbe";
    public static final String CRYPTO_ETH_KEY = "crypto.ETH.address";

    private final Web3j web3j;
    private final int addressLength;
    private long syncThreshold; // non-final in case this value needs to be tweaked

    public EnsResolver(Web3j web3j, long syncThreshold, int addressLength) {
        this.web3j = web3j;
        this.syncThreshold = syncThreshold;
        this.addressLength = addressLength;
    }

    public EnsResolver(Web3j web3j, long syncThreshold) {
        this(web3j, syncThreshold, Keys.ADDRESS_LENGTH_IN_HEX);
    }

    public EnsResolver(Web3j web3j) {
        this(web3j, DEFAULT_SYNC_THRESHOLD);
    }

    public void setSyncThreshold(long syncThreshold) {
        this.syncThreshold = syncThreshold;
    }

    public long getSyncThreshold() {
        return syncThreshold;
    }

    /**
     * This function takes ensName (eg 'scotty.eth') and returns the matching Ethereum Address.
     * NOTE: It is highly important to check the node is synced before resolving, as this could be an attack
     * @param contractId
     * @return
     */
    public String resolve(String contractId)
    {
        String contractAddress = contractId;
        if (isValidEnsName(contractId, addressLength))
        {
            try
            {
                if (!isSynced()) //ensure node is synced
                {
                    throw new EnsResolutionException("Node is not currently synced");
                }
                else if (contractId.endsWith(".crypto")) //check crypto namespace
                {
                    byte[] nameHash = NameHash.nameHashAsBytes(contractId);
                    BigInteger nameId = new BigInteger(nameHash);
                    String resolverAddress = getContractData(EthereumNetworkBase.MAINNET_ID, CRYPTO_RESOLVER, getResolverOf(nameId));
                    if (!TextUtils.isEmpty(resolverAddress))
                    {
                        contractAddress = getContractData(EthereumNetworkBase.MAINNET_ID, resolverAddress, get(nameId));
                    }
                }
                else
                {
                    String resolverAddress = lookupResolver(contractId);
                    if (!TextUtils.isEmpty(resolverAddress))
                    {
                        byte[] nameHash = NameHash.nameHashAsBytes(contractId);
                        //now attempt to get the address of this ENS
                        contractAddress = getContractData(EthereumNetworkBase.MAINNET_ID, resolverAddress, getAddr(nameHash));
                    }
                }
            }
            catch (Exception e)
            {
                throw new RuntimeException("Unable to execute Ethereum request", e);
            }

            if (!WalletUtils.isValidAddress(contractAddress))
            {
                throw new RuntimeException("Unable to resolve address for name: " + contractId);
            }
            else
            {
                return contractAddress;
            }
        }
        else
        {
            return contractId;
        }
    }

    /**
     * Reverse name resolution as documented in the <a
     * href="https://docs.ens.domains/contract-api-reference/reverseregistrar">specification</a>.
     *
     * @param address an ethereum address, example: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
     * @return a EnsName registered for provided address
     */
    public String reverseResolve(String address) throws UnableToResolveENS
    {
        String name = null;
        if (WalletUtils.isValidAddress(address))
        {
            String reverseName = Numeric.cleanHexPrefix(address) + REVERSE_NAME_SUFFIX;
            try
            {
                String resolverAddress = lookupResolver(reverseName);
                byte[] nameHash = NameHash.nameHashAsBytes(reverseName);
                name = getContractData(EthereumNetworkBase.MAINNET_ID, resolverAddress, getName(nameHash));
            }
            catch (Exception e)
            {
                throw new RuntimeException("Unable to execute Ethereum request", e);
            }

            if (!isValidEnsName(name, addressLength))
            {
                throw new UnableToResolveENS("Unable to resolve name for address: " + address);
            }
            else
            {
                return name;
            }
        }
        else
        {
            throw new EnsResolutionException("Address is invalid: " + address);
        }
    }

    private String lookupResolver(String ensName) throws Exception
    {
        NetVersion netVersion = web3j.netVersion().send();
        String registryContract = Contracts.resolveRegistryContract(netVersion.getNetVersion());
        byte[] nameHash = NameHash.nameHashAsBytes(ensName);
        Function resolver = getResolver(nameHash);
        return getContractData(EthereumNetworkBase.MAINNET_ID, registryContract, resolver);
    }

    private Function getResolver(byte[] nameHash)
    {
        return new Function("resolver",
                            Arrays.<Type>asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)),
                            Arrays.<TypeReference<?>>asList(new TypeReference<Address>()
                            {
                            }));
    }

    private Function getResolverOf(BigInteger nameId)
    {
        return new Function("resolverOf",
                            Arrays.<Type>asList(new org.web3j.abi.datatypes.Uint(nameId)),
                            Arrays.<TypeReference<?>>asList(new TypeReference<Address>()
                            {
                            }));
    }

    private Function get(BigInteger nameId)
    {
        return new Function("get",
                            Arrays.<Type>asList(new org.web3j.abi.datatypes.Utf8String(EnsResolver.CRYPTO_ETH_KEY), new org.web3j.abi.datatypes.generated.Uint256(nameId)),
                            Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>()
                            {
                            }));
    }

    private Function getAddr(byte[] nameHash)
    {
        return new Function("addr",
                            Arrays.<Type>asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)),
                            Arrays.<TypeReference<?>>asList(new TypeReference<Address>()
                            {
                            }));
    }

    private Function getName(byte[] nameHash)
    {
        return new Function("name",
                            Arrays.<Type>asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)),
                            Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>()
                            {
                            }));
    }

    boolean isSynced() throws Exception {
        EthSyncing ethSyncing = web3j.ethSyncing().send();
        if (ethSyncing.isSyncing()) {
            return false;
        } else {
            EthBlock ethBlock =
                    web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).send();
            long timestamp = ethBlock.getBlock().getTimestamp().longValue() * 1000;

            return System.currentTimeMillis() - syncThreshold < timestamp;
        }
    }

    private String callSmartContractFunction(
            Function function, String contractAddress, int chainId) throws Exception
    {
        try
        {
            String encodedFunction = FunctionEncoder.encode(function);

            org.web3j.protocol.core.methods.request.Transaction transaction
                    = createEthCallTransaction(TokenscriptFunction.ZERO_ADDRESS, contractAddress, encodedFunction);
            EthCall response = TokenRepository.getWeb3jService(chainId).ethCall(transaction, DefaultBlockParameterName.LATEST).send();

            return response.getValue();
        }
        catch (InterruptedIOException | UnknownHostException e)
        {
            //expected to happen when user switches wallets
            return "0x";
        }
    }

    private <T> T getContractData(int chainId, String address, Function function) throws Exception
    {
        String responseValue = callSmartContractFunction(function, address, chainId);

        if (TextUtils.isEmpty(responseValue))
        {
            throw new Exception("Bad contract value");
        }
        else if (responseValue.equals("0x"))
        {
            return null;
        }

        List<Type> response = FunctionReturnDecoder.decode(
                responseValue, function.getOutputParameters());
        if (response.size() == 1)
        {
            return (T) response.get(0).getValue();
        }
        else
        {
            return null;
        }
    }

    public static boolean isValidEnsName(String input) {
        return isValidEnsName(input, Keys.ADDRESS_LENGTH_IN_HEX);
    }

    public static boolean isValidEnsName(String input, int addressLength) {
        return input != null // will be set to null on new Contract creation
                && (input.contains(".") || !WalletUtils.isValidAddress(input));
    }
}