package com.r35157.libs.solana.impl.cached;

import com.r35157.libs.objcache.ObjectCache;
import com.r35157.libs.objcache.ObjectCacheKey;
import com.r35157.libs.solana.SPLTokenHolding;
import com.r35157.libs.solana.SPLTokenSupply;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaCommitment;
import com.r35157.libs.solana.SolanaLatestBlockhash;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import com.r35157.libs.solana.SolanaProgramAddressSeed;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.SolanaTransactionOutcome;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public final class CachedSolanaBlockChain implements SolanaBlockChain {

    public CachedSolanaBlockChain(
            SolanaBlockChain delegate,
            ObjectCache objectCache
    ) {
        this.delegate = Objects.requireNonNull(delegate);
        this.objectCache = Objects.requireNonNull(objectCache);
    }

    @Override
    public ΩSolanaAmountΩ getBalanceInSolana(
            ΩSolanaAddressΩ address
    ) throws IOException, InterruptedException {
        return delegate.getBalanceInSolana(address);
    }

    @Override
    public ΩlamportsΩ getBalanceInLamport(
            ΩSolanaAddressΩ address
    ) throws IOException, InterruptedException {
        return delegate.getBalanceInLamport(address);
    }

    @Override
    public SolanaLatestBlockhash getLatestBlockhash()
            throws IOException, InterruptedException {
        return delegate.getLatestBlockhash();
    }

    @Override
    public ΩlamportsΩ getFeeForMessage(
            ΩBase64StringΩ serializedMessage
    ) throws IOException, InterruptedException {
        return delegate.getFeeForMessage(serializedMessage);
    }

    @Override
    public SolanaUnsignedTransaction buildSolanaTransferTransaction(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient,
            ΩSolanaAmountΩ amount
    ) throws IOException, InterruptedException {
        return delegate.buildSolanaTransferTransaction(
                sender,
                recipient,
                amount
        );
    }

    @Override
    public SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient
    ) throws IOException, InterruptedException {
        return delegate.buildSolanaTransferAllTransaction(
                sender,
                recipient
        );
    }

    @Override
    public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
            ΩSolanaAddressΩ ownerAddress,
            SolanaSPLTokenProgram splProgramId
    ) throws IOException, InterruptedException {
        long t0 = System.currentTimeMillis();
        Map<ΩSPLMintAddressΩ, SPLTokenHolding> result = delegate.getSPLTokenHoldings(
                ownerAddress,
                splProgramId
        );
        System.out.println("**F** LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms");
        return result;
    }

    @Override
    public Set<ΩSolanaNFTAddressΩ> getSolanaNFTCandidateAddresses(
            ΩSolanaAddressΩ ownerAddress,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {
        long t0 = System.currentTimeMillis();
        Set<ΩSolanaNFTAddressΩ> result = delegate.getSolanaNFTCandidateAddresses(
                ownerAddress,
                splProgram
        );
        System.out.println("@@@E@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms");
        return result;
    }

    @Override
    public SolanaProgramDerivedAddress findProgramAddress(
            ΩSolanaProgramIdΩ programId,
            List<SolanaProgramAddressSeed> seeds
    ) {
        long t0 = System.currentTimeMillis();
        SolanaProgramDerivedAddress addr = delegate.findProgramAddress(
                programId,
                seeds
        );
        System.out.println("@@@D@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms");
        return addr;
    }

    @Override
    public @Nullable SolanaAccountInfo getAccountInfo(
            ΩSolanaAddressΩ accountAddress
    ) throws IOException, InterruptedException {
        long t0 = System.currentTimeMillis();
        SolanaAccountInfo result;

        ObjectCacheKey key = new ObjectCacheKey(
                SolanaBlockChain.class.getName(),
                "getAccountInfo",
                accountAddress
        );

        Object cachedObject = objectCache.get(key);

        result = (cachedObject != null)
                ? castAccountInfo(cachedObject)
                : loadAccountInfo(
                key,
                accountAddress
        );

        System.out.println("@@@C@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms");
        return result;
    }

    @Override
    public ΩSolanaAddressΩ encodeSolanaAddress(
            byte[] addressBytes
    ) {
        return delegate.encodeSolanaAddress(addressBytes);
    }

    @Override
    public SPLTokenSupply getSPLTokenSupply(
            ΩSPLMintAddressΩ mintAddress,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {
        long t0 = System.currentTimeMillis();
        SPLTokenSupply supply = delegate.getSPLTokenSupply(
                mintAddress,
                splProgram
        );
        System.out.println("@@@B@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms");
        return supply;
    }

    @Override
    public Set<SolanaAccountInfo> getProgramAccounts(
            ΩSolanaProgramIdΩ programId,
            Set<SolanaProgramAccountMemcmpFilter> filters
    ) throws IOException, InterruptedException {
        long t0 = System.currentTimeMillis();
        Set<SolanaAccountInfo> result;
        Set<SolanaProgramAccountMemcmpFilter> copiedFilters =
                Set.copyOf(filters);

        ObjectCacheKey key = new ObjectCacheKey(
                SolanaBlockChain.class.getName(),
                "getProgramAccounts",
                programId,
                copiedFilters
        );

        Object cachedObject = objectCache.get(key);

        result = (cachedObject != null)
                ? castProgramAccounts(cachedObject)
                : loadProgramAccounts(
                    key,
                    programId,
                    copiedFilters
                );

        System.out.println("@@@A@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms");
        return result;
    }

    @Override
    public ΩSolanaTransactionSignatureΩ sendTransaction(
            SolanaSignedTransaction transaction
    ) throws IOException, InterruptedException {
        return delegate.sendTransaction(transaction);
    }

    @Override
    public SolanaTransactionOutcome awaitTransaction(
            ΩSolanaTransactionSignatureΩ signature,
            SolanaCommitment commitment,
            Duration timeout
    ) throws IOException, InterruptedException {
        return delegate.awaitTransaction(signature, commitment, timeout);
    }

    @Override
    public SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
            ΩSolanaAddressΩ sender,
            ΩSPLTokenAccountΩ senderTokenAccount,
            ΩSPLMintAddressΩ mintAddress,
            ΩSolanaAddressΩ recipient,
            ΩSPLTokenAccountΩ recipientTokenAccount,
            ΩRawAmountΩ rawAmount,
            ΩamountDecimalsΩ decimals,
            SolanaSPLTokenProgram splProgram,
            boolean createRecipientTokenAccount
    ) throws IOException, InterruptedException {
        return delegate.buildSPLTokenTransferTransaction(
                sender,
                senderTokenAccount,
                mintAddress,
                recipient,
                recipientTokenAccount,
                rawAmount,
                decimals,
                splProgram,
                createRecipientTokenAccount
        );
    }

    @Override
    public SolanaUnsignedTransaction buildSPLTokenBurnTransaction(
            ΩSolanaAddressΩ owner,
            ΩSPLTokenAccountΩ tokenAccount,
            ΩSPLMintAddressΩ mintAddress,
            ΩRawAmountΩ rawAmount,
            ΩamountDecimalsΩ decimals,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {
        return delegate.buildSPLTokenBurnTransaction(
                owner,
                tokenAccount,
                mintAddress,
                rawAmount,
                decimals,
                splProgram
        );
    }

    @Override
    public ΩSPLTokenAccountΩ findAssociatedTokenAccount(
            ΩSolanaAddressΩ owner,
            ΩSPLMintAddressΩ mintAddress,
            SolanaSPLTokenProgram splProgram
    ) {
        return delegate.findAssociatedTokenAccount(
                owner,
                mintAddress,
                splProgram
        );
    }

    private synchronized Set<SolanaAccountInfo> loadProgramAccounts(
            ObjectCacheKey key,
            ΩSolanaProgramIdΩ programId,
            Set<SolanaProgramAccountMemcmpFilter> filters
    ) throws IOException, InterruptedException {
        Object cachedObject = objectCache.get(key);

        if (cachedObject != null) {
            return castProgramAccounts(cachedObject);
        }

        Set<SolanaAccountInfo> programAccounts = Set.copyOf(
                delegate.getProgramAccounts(
                        programId,
                        filters
                )
        );

        objectCache.put(
                key,
                programAccounts,
                PROGRAM_ACCOUNTS_TTL_MILLIS
        );

        return programAccounts;
    }

    private synchronized SolanaAccountInfo loadAccountInfo(
            ObjectCacheKey key,
            ΩSolanaAddressΩ accountAddress
    ) throws IOException, InterruptedException {
        Object cachedObject = objectCache.get(key);

        if (cachedObject != null) {
            return castAccountInfo(cachedObject);
        }

        SolanaAccountInfo accountInfo = delegate.getAccountInfo(accountAddress);

        objectCache.put(
                key,
                accountInfo,
                ACCOUNT_INFO_TTL_MILLIS
        );

        return accountInfo;
    }

    @SuppressWarnings("unchecked")
    private Set<SolanaAccountInfo> castProgramAccounts(
            Object object
    ) {
        return (Set<SolanaAccountInfo>) object;
    }

    @SuppressWarnings("unchecked")
    private SolanaAccountInfo castAccountInfo(
            Object object
    ) {
        return (SolanaAccountInfo) object;
    }

    private static final long ACCOUNT_INFO_TTL_MILLIS       = 60_000;
    private static final long PROGRAM_ACCOUNTS_TTL_MILLIS   = 60_000;

    private final SolanaBlockChain delegate;
    private final ObjectCache objectCache;
}
