package com.alphawallet.app.viewmodel;

import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.text.format.DateUtils;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import com.alphawallet.app.C;
import com.alphawallet.app.entity.ContractType;
import com.alphawallet.app.entity.NetworkInfo;
import com.alphawallet.app.entity.QRResult;
import com.alphawallet.app.entity.Wallet;
import com.alphawallet.app.entity.tokens.Token;
import com.alphawallet.app.entity.tokens.TokenCardMeta;
import com.alphawallet.app.entity.tokens.TokenInfo;
import com.alphawallet.app.interact.FetchTransactionsInteract;
import com.alphawallet.app.interact.GenericWalletInteract;
import com.alphawallet.app.repository.EthereumNetworkRepositoryType;
import com.alphawallet.app.service.AssetDefinitionService;
import com.alphawallet.app.service.TokensService;
import com.alphawallet.app.ui.ImportTokenActivity;
import com.alphawallet.app.ui.SendActivity;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import javax.annotation.Nullable;
import javax.inject.Inject;

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

@HiltViewModel
public class AddTokenViewModel extends BaseViewModel
{

    private final MutableLiveData<Wallet> wallet = new MutableLiveData<>();
    private final MutableLiveData<Long> switchNetwork = new MutableLiveData<>();
    private final MutableLiveData<Token> finalisedToken = new MutableLiveData<>();
    private final MutableLiveData<Token> tokenType = new MutableLiveData<>();
    private final MutableLiveData<Boolean> noContract = new MutableLiveData<>();
    private final MutableLiveData<Integer> scanCount = new MutableLiveData<>();

    private final MutableLiveData<Token> onToken = new MutableLiveData<>();
    private final MutableLiveData<Token[]> allTokens = new MutableLiveData<>();

    private final EthereumNetworkRepositoryType ethereumNetworkRepository;
    private final GenericWalletInteract genericWalletInteract;
    private final FetchTransactionsInteract fetchTransactionsInteract;
    private final AssetDefinitionService assetDefinitionService;
    private final TokensService tokensService;

    private boolean foundNetwork;
    private int networkCount;
    private long primaryChainId = 1;
    private final List<Token> discoveredTokenList = new ArrayList<>();
    private final Handler handler = new Handler(Looper.getMainLooper());

    public MutableLiveData<Wallet> wallet()
    {
        return wallet;
    }

    public MutableLiveData<Token> tokenType()
    {
        return tokenType;
    }

    public LiveData<Long> switchNetwork()
    {
        return switchNetwork;
    }

    public LiveData<Integer> chainScanCount()
    {
        return scanCount;
    }

    public LiveData<Token> onToken()
    {
        return onToken;
    }

    public LiveData<Token[]> allTokens()
    {
        return allTokens;
    }

    @Nullable
    Disposable scanNetworksDisposable;

    private final List<Disposable> scanThreads = new ArrayList<>();

    @Inject
    AddTokenViewModel(
            GenericWalletInteract genericWalletInteract,
            EthereumNetworkRepositoryType ethereumNetworkRepository,
            FetchTransactionsInteract fetchTransactionsInteract,
            AssetDefinitionService assetDefinitionService,
            TokensService tokensService)
    {
        this.genericWalletInteract = genericWalletInteract;
        this.ethereumNetworkRepository = ethereumNetworkRepository;
        this.fetchTransactionsInteract = fetchTransactionsInteract;
        this.assetDefinitionService = assetDefinitionService;
        this.tokensService = tokensService;
    }

    public void saveTokens(List<Token> toSave)
    {
        tokensService.addTokens(toSave);
    }

    @Override
    protected void onCleared()
    {
        super.onCleared();
        if (scanNetworksDisposable != null && !scanNetworksDisposable.isDisposed()) scanNetworksDisposable.dispose();
    }

    public void setPrimaryChain(long chainId)
    {
        primaryChainId = chainId;
    }

    public long getSelectedChain()
    {
        return primaryChainId;
    }

    private void checkType(Throwable throwable, long chainId, String address, ContractType type)
    {
        if (type == ContractType.ERC1155)
        {
            onTokensSetup(new TokenInfo(address, "Holding Contract", "", 0, true, chainId));
        }
        else
        {
            onError(throwable);
        }
    }

    public void fetchToken(long chainId, String addr)
    {
        tokensService.update(addr, chainId, ContractType.NOT_SET)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::gotTokenUpdate, this::onError).isDisposed();
    }

    private void gotTokenUpdate(TokenInfo tokenInfo)
    {
        disposable = tokensService.addToken(tokenInfo, wallet.getValue().address)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::resumeSend, this::onError);
    }

    private void resumeSend(Token token)
    {
        finalisedToken.postValue(token);
    }

    public NetworkInfo getNetworkInfo(long chainId)
    {
        return ethereumNetworkRepository.getNetworkByChain(chainId);
    }

    private void findWallet()
    {
        disposable = genericWalletInteract.find()
                .subscribe(wallet::setValue, this::onError);
    }

    private void onTokensSetup(TokenInfo info)
    {
        disposable = tokensService.addToken(info, wallet.getValue().address)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::finaliseToken, error -> tokenTypeError(error, info));
    }

    private void finaliseToken(Token token)
    {
        checkNetworkCount();
        discoveredTokenList.add(token);
        onToken.postValue(token);
    }

    private void tokenTypeError(Throwable throwable, TokenInfo data)
    {
        checkNetworkCount();
        Token badToken = new Token(data, BigDecimal.ZERO, 0, "", ContractType.NOT_SET);
        tokenType.postValue(badToken);
    }

    public void prepare()
    {
        findWallet();
    }

    public void showSend(Context ctx, QRResult result, Token token)
    {
        Intent intent = new Intent(ctx, SendActivity.class);
        boolean sendingTokens = (result.getFunction() != null && result.getFunction().length() > 0);
        String address = wallet.getValue().address;
        int decimals = 18;

        if (sendingTokens)
        {
            address = result.getAddress();
            decimals = token.tokenInfo.decimals;
        }

        intent.putExtra(C.EXTRA_SENDING_TOKENS, sendingTokens);
        intent.putExtra(C.EXTRA_CONTRACT_ADDRESS, address);
        intent.putExtra(C.EXTRA_NETWORKID, token.tokenInfo.chainId);
        intent.putExtra(C.EXTRA_SYMBOL, ethereumNetworkRepository.getNetworkByChain(result.chainId).symbol);
        intent.putExtra(C.EXTRA_DECIMALS, decimals);
        intent.putExtra(C.Key.WALLET, wallet.getValue());
        intent.putExtra(C.EXTRA_AMOUNT, result);
        intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
        ctx.startActivity(intent);
    }

    private List<Long> getNetworkIds()
    {
        List<Long> networkIds = new ArrayList<>();
        networkIds.add(primaryChainId); //test selected chain first
        for (long chainId : tokensService.getNetworkFilters())
        {
            if (!networkIds.contains(chainId)) networkIds.add(chainId);
        }

        //Now scan unselected networks
        for (NetworkInfo networkInfo : ethereumNetworkRepository.getAvailableNetworkList())
        {
            if (!networkIds.contains(networkInfo.chainId)) networkIds.add(networkInfo.chainId);
        }

        return networkIds;
    }

    public void testNetworks(String address)
    {
        foundNetwork = false;
        discoveredTokenList.clear();
        networkCount = ethereumNetworkRepository.getAvailableNetworkList().length;
        scanCount.postValue(networkCount);

        ethereumNetworkRepository.getAllActiveNetworks();
        scanCount.postValue(networkCount);

        for (long networkId : getNetworkIds())
        {
            TokenInfo tokenInfo = new TokenInfo(address, "", "", 0, true, networkId);
            Disposable d = fetchTransactionsInteract.queryInterfaceSpec(tokenInfo)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(type -> testNetworkResult(tokenInfo, type), this::onTestError);

            scanThreads.add(d);
        }

        handler.postDelayed(this::stopScan, 60 * DateUtils.SECOND_IN_MILLIS);
    }

    private void testNetworkResult(final TokenInfo info, final ContractType type)
    {
        if (type != ContractType.OTHER)
        {
            foundNetwork = true;
            disposable = tokensService
                    .update(info.address, info.chainId, type)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(this::onTokensSetup, error -> checkType(error, info.chainId, info.address, type));
        }
        else
        {
            checkNetworkCount();
        }
    }

    public void stopScan()
    {
        for (Disposable d : scanThreads)
        {
            if (!d.isDisposed()) d.dispose();
        }
        scanThreads.clear();
        scanCount.postValue(0);
        handler.removeCallbacksAndMessages(null);
    }

    private void onTestError(Throwable throwable)
    {
        checkNetworkCount();
        onError(throwable);
    }

    private void checkNetworkCount()
    {
        networkCount--;
        scanCount.postValue(networkCount);
        if (networkCount == 0 && !foundNetwork)
        {
            noContract.postValue(true);
        }
        if (networkCount == 0 && discoveredTokenList.size() > 0)
        {
            allTokens.postValue(discoveredTokenList.toArray(new Token[0]));
        }
    }

    public void showImportLink(Context context, String importTxt)
    {
        Intent intent = new Intent(context, ImportTokenActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra(C.IMPORT_STRING, importTxt);
        context.startActivity(intent);
    }

    public Token getToken(long chainId, String address)
    {
        return tokensService.getToken(chainId, address);
    }

    public TokensService getTokensService()
    {
        return tokensService;
    }

    public AssetDefinitionService getAssetDefinitionService()
    {
        return assetDefinitionService;
    }

    public EthereumNetworkRepositoryType ethereumNetworkRepository()
    {
        return ethereumNetworkRepository;
    }

    public void selectExtraChains(List<Long> selectedChains)
    {
        //add new chains to chain selection
        //get current list and add it on
        HashSet<Long> uniqueList = new HashSet<>(selectedChains);
        uniqueList.addAll(ethereumNetworkRepository.getFilterNetworkList());
        ethereumNetworkRepository.setFilterNetworkList(uniqueList.toArray(new Long[0]));
        ethereumNetworkRepository.commitPrefs();
        tokensService.setupFilter(true);
    }

    /**
     * Set all selected tokens enabled and visible.
     * Note that we need to update the 'visibility changed' setting to mark the token as having explicitly been set visible.
     * @param selected list of selected TCMs
     */
    public void markTokensEnabled(List<TokenCardMeta> selected)
    {
        if (wallet.getValue() == null) return;
        Observable.fromIterable(selected)
                .forEach(tcm -> tokensService.enableToken(wallet.getValue().address, tcm.getContractAddress())
                        .subscribeOn(Schedulers.io())
                        .observeOn(Schedulers.io())
                        .subscribe())
                .isDisposed();
    }
}
