package com.alphawallet.app.viewmodel;

import android.app.Activity;
import android.os.NetworkOnMainThreadException;

import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import com.alphawallet.app.entity.MessagePair;
import com.alphawallet.app.entity.Operation;
import com.alphawallet.app.entity.SignAuthenticationCallback;
import com.alphawallet.app.entity.SignaturePair;
import com.alphawallet.app.entity.TransferFromEventResponse;
import com.alphawallet.app.entity.Wallet;
import com.alphawallet.hardware.SignatureFromKey;
import com.alphawallet.hardware.SignatureReturnType;
import com.alphawallet.app.entity.tokens.Ticket;
import com.alphawallet.app.entity.tokens.Token;
import com.alphawallet.app.interact.CreateTransactionInteract;
import com.alphawallet.app.interact.FetchTokensInteract;
import com.alphawallet.app.interact.GenericWalletInteract;
import com.alphawallet.app.interact.MemPoolInteract;
import com.alphawallet.app.interact.SignatureGenerateInteract;
import com.alphawallet.app.service.AssetDefinitionService;
import com.alphawallet.app.service.KeyService;
import com.alphawallet.app.service.TokensService;
import com.alphawallet.token.entity.TicketRange;

import org.web3j.abi.datatypes.generated.Uint16;
import org.web3j.utils.Numeric;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import dagger.hilt.android.lifecycle.HiltViewModel;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

/**
 * Created by James on 25/01/2018.
 */
@HiltViewModel
public class RedeemSignatureDisplayModel extends BaseViewModel
{
    private static final long CYCLE_SIGNATURE_INTERVAL = 10 * 60; //cycle every 10 minutes
    private static final long CHECK_BALANCE_INTERVAL = 10;

    private final KeyService keyService;
    private final GenericWalletInteract genericWalletInteract;
    private final SignatureGenerateInteract signatureGenerateInteract;
    private final CreateTransactionInteract createTransactionInteract;
    private final FetchTokensInteract fetchTokensInteract;
    private final MemPoolInteract memoryPoolInteract;
    private final TokensService tokensService;
    private final AssetDefinitionService assetDefinitionService;

    private final MutableLiveData<Wallet> defaultWallet = new MutableLiveData<>();
    private final MutableLiveData<SignaturePair> signature = new MutableLiveData<>();

    private final MutableLiveData<String> selection = new MutableLiveData<>();
    private final MutableLiveData<Boolean> burnNotice = new MutableLiveData<>();
    private final MutableLiveData<Boolean> signRequest = new MutableLiveData<>();

    private Disposable memPoolSubscription;
    private final List<BigInteger> tickets = new ArrayList<>();
    private Token token;

    @Nullable
    private Disposable getBalanceDisposable;

    @Nullable
    private Disposable cycleSignatureDisposable;

    private String address;

    @Inject
    RedeemSignatureDisplayModel(
            GenericWalletInteract genericWalletInteract,
            SignatureGenerateInteract signatureGenerateInteract,
            CreateTransactionInteract createTransactionInteract,
            KeyService keyService,
            FetchTokensInteract fetchTokensInteract,
            MemPoolInteract memoryPoolInteract,
            TokensService tokensService,
            AssetDefinitionService assetDefinitionService) {
        this.genericWalletInteract = genericWalletInteract;
        this.signatureGenerateInteract = signatureGenerateInteract;
        this.keyService = keyService;
        this.createTransactionInteract = createTransactionInteract;
        this.fetchTokensInteract = fetchTokensInteract;
        this.memoryPoolInteract = memoryPoolInteract;
        this.tokensService = tokensService;
        this.assetDefinitionService = assetDefinitionService;
    }

    public LiveData<Wallet> defaultWallet() {
        return defaultWallet;
    }
    public LiveData<SignaturePair> signature() {
        return signature;
    }
    public LiveData<String> selection() {
        return selection;
    }
    public LiveData<Boolean> burnNotice() {
        return burnNotice;
    }
    public LiveData<Boolean> signRequest() {
        return signRequest;
    }

    public TokensService getTokensService() { return tokensService; }

    @Override
    protected void onCleared() {
        super.onCleared();

        if (cycleSignatureDisposable != null) {
            cycleSignatureDisposable.dispose();
        }
        if (getBalanceDisposable != null) {
            getBalanceDisposable.dispose();
        }
        if (memPoolSubscription != null && !memPoolSubscription.isDisposed()) {
            closeListener()
                    .subscribeOn(Schedulers.newThread())
                    .subscribe();
        }
    }

    private void fetchTokenBalance() {
        progress.postValue(true);
        getBalanceDisposable = Observable.interval(CHECK_BALANCE_INTERVAL, CHECK_BALANCE_INTERVAL, TimeUnit.SECONDS)
                .doOnNext(l -> fetchTokensInteract
                        .updateDefaultBalance(token, defaultWallet.getValue())
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(this::onToken, this::onError)).subscribe();
    }

    private void checkRedeemTicket() {
        progress.postValue(true);
        getBalanceDisposable = Observable.interval(CHECK_BALANCE_INTERVAL, CHECK_BALANCE_INTERVAL, TimeUnit.SECONDS)
                .doOnNext(l -> fetchTokensInteract
                        .checkRedeemed(token, this.tickets)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(this::onRedeemCheck, this::onError)).subscribe();
    }

    private void onRedeemCheck(Boolean redeemed)
    {
        if (redeemed)
        {
            ticketsBurned();
        }
    }

    /**
     * This is a fallback check - it polls balance. If we somehow miss the memory pool pickup
     * then this method will pick up when the transaction is written to blockchain
     */
    private void onToken(Token token)
    {
        this.token = token;

        if (token != null && token.tokenInfo.address.equalsIgnoreCase(address) && token.hasArrayBalance())
        {
            boolean allBurned = true;
            List<BigInteger> balance = token.getArrayBalance();
            //See if our indices got burned
            if (balance != null)
            {
                for (BigInteger index : this.tickets)
                {
                    if (token.isERC721Ticket() && !index.equals(BigInteger.ZERO)) //handle ERC721
                    {
                        allBurned = false;
                        break;
                    }
                    else if (!balance.get(index.intValue()).equals(BigInteger.ZERO))
                    {
                        allBurned = false;
                        break;
                    }
                }
            }

            if (allBurned)
            {
                ticketsBurned();
            }
        }
    }

    private void ticketsBurned()
    {
        if (cycleSignatureDisposable != null) {
            cycleSignatureDisposable.dispose();
        }
        if (getBalanceDisposable != null) {
            getBalanceDisposable.dispose();
        }
        if (!memPoolSubscription.isDisposed()) {
            closeListener()
                    .subscribeOn(Schedulers.newThread())
                    .subscribe();
        }

        tickets.clear();
        burnNotice.postValue(true);
    }

    private Single<Boolean> closeListener()
    {
        return Single.fromCallable(() -> {
            try {
                memPoolSubscription.dispose();
                return true;
            } catch (NetworkOnMainThreadException th) {
                // Ignore all errors, it's not important source.
                return false;
            }
        });
    }

    public void prepare(String address, Token ticket, TicketRange ticketRange) {
        this.address = address;
        token = ticket;
        tickets.clear();
        if(ticket instanceof Ticket)
        {
            this.tickets.addAll(((Ticket)ticket).ticketIdListToIndexList(ticketRange.tokenIds));
        }
        else
        {
            this.tickets.addAll(ticketRange.tokenIds);
        }
        disposable = genericWalletInteract
                .find()
                .subscribe(this::onDefaultWallet, this::onError);
    }

    private void startCycleSignature() {
        cycleSignatureDisposable = Observable.interval(0, CYCLE_SIGNATURE_INTERVAL, TimeUnit.SECONDS)
                .doOnNext(l -> signRequest.postValue(true))
                .subscribe(l -> {}, t -> {});
    }

    public void updateSignature(Wallet wallet)
    {
        signatureGenerateInteract
                .getMessage(tickets, token.getAddress(), this.token.getInterfaceSpec())
                .subscribe(pair -> onSignMessage(pair, wallet), this::onError)
                .isDisposed();
    }

    private void startMemoryPoolListener() {
        memPoolSubscription = memoryPoolInteract.burnListener(token.getAddress())
                .subscribeOn(Schedulers.newThread())
                .subscribe(this::receiveBurnNotification, this::onBurnError);
    }

    private void receiveBurnNotification(TransferFromEventResponse burnTx)
    {
        String userAddr = Numeric.cleanHexPrefix(defaultWallet().getValue().address).toLowerCase();

        if (burnTx._from.toLowerCase().contains(userAddr))
        {
            List<Uint16> transferIndicies = burnTx._indices;
            markUsedIndicies(transferIndicies);
        }
    }

    //restart the listener - sometimes blockchain throws a wobbly
    private void onBurnError(Throwable throwable)
    {
        if (!memPoolSubscription.isDisposed()) memPoolSubscription.dispose();
        startMemoryPoolListener();
    }

    private MessagePair pairForHardwareSign;

    private void onSignMessage(MessagePair pair, Wallet wallet) {
        //now run this guy through the signed message system
        if (pair != null)
        {
            pairForHardwareSign = pair;
            disposable = createTransactionInteract
                    .sign(wallet, pair)
                    .subscribe(this::onSignedMessage, this::onError);
        }
    }

    public void completeHardwareSign(SignatureFromKey signatureFromKey)
    {
        signature.postValue(new SignaturePair(pairForHardwareSign.selection,
                signatureFromKey, pairForHardwareSign.message));
    }

    private void onSignedMessage(SignaturePair sigPair)
    {
        if (sigPair.signature.sigType == SignatureReturnType.SIGNING_POSTPONED) return;
        signature.postValue(sigPair);
    }

    private void onDefaultWallet(Wallet wallet) {
        defaultWallet.setValue(wallet);
        startCycleSignature();
        if (token.isERC721Ticket())
        {
            checkRedeemTicket();
        }
        else
        {
            fetchTokenBalance();
        }
        startMemoryPoolListener();

        onSaved();
    }

    private void markUsedIndicies(List<Uint16> burnList) {
        for (Uint16 indexVal : burnList)
        {
            Integer index = indexVal.getValue().intValue();
            tickets.remove(index);
        }
    }

    private void onSaved()
    {
        //display 'burn complete'
        if (tickets.size() == 0)
        {
            ticketsBurned();
        }
    }

    public AssetDefinitionService getAssetDefinitionService()
    {
        return assetDefinitionService;
    }

    public void getAuthorisation(Activity activity, SignAuthenticationCallback callback)
    {
        if (defaultWallet.getValue() != null)
        {
            keyService.getAuthenticationForSignature(defaultWallet.getValue(), activity, callback);
        }
    }

    public void resetSignDialog()
    {
        keyService.resetSigningDialog();
    }

    public void completeAuthentication(Operation signData)
    {
        keyService.completeAuthentication(signData);
    }
}
