package com.r35157.libs.jupiter.perps.impl.anchoridl;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.jupiter.perps.protocol.DecreasePositionRequest;
import com.r35157.libs.jupiter.perps.protocol.DecreasePositionResponse;
import com.r35157.libs.jupiter.perps.protocol.ExecuteTransactionRequest;
import com.r35157.libs.jupiter.perps.protocol.ExecuteTransactionResponse;
import com.r35157.libs.jupiter.perps.protocol.IncreasePositionRequest;
import com.r35157.libs.jupiter.perps.protocol.IncreasePositionResponse;
import com.r35157.libs.jupiter.perps.protocol.TransactionMetadata;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaCommitment;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.SolanaTransactionOutcome;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
import static java.math.BigDecimal.ZERO;

public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {

    /**
     * Creates a Jupiter Perps service with a two-minute transaction
     * confirmation timeout.
     *
     * @param solanaBlockChain blockchain used for account access and
     *                         transaction confirmation
     * @param wallet wallet used to sign Perps transactions
     * @param currencyIdentityService canonical currency identity service
     */
    public AnchorIdlJupiterPerpsServiceImpl(
            @NotNull SolanaBlockChain solanaBlockChain,
            @NotNull SolanaWallet wallet,
            @NotNull CurrencyIdentityService currencyIdentityService
    ) {
        this(
                solanaBlockChain,
                wallet,
                currencyIdentityService,
                DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT
        );
    }

    /**
     * Creates a Jupiter Perps service with an explicit confirmation timeout.
     *
     * @param solanaBlockChain blockchain used for account access and
     *                         transaction confirmation
     * @param wallet wallet used to sign Perps transactions
     * @param currencyIdentityService canonical currency identity service
     * @param transactionConfirmationTimeout positive timeout applied when
     *        independently awaiting submitted transaction signatures
     * @throws NullPointerException if an argument is {@code null}
     * @throws IllegalArgumentException if the timeout is zero or negative
     */
    public AnchorIdlJupiterPerpsServiceImpl(
            @NotNull SolanaBlockChain solanaBlockChain,
            @NotNull SolanaWallet wallet,
            @NotNull CurrencyIdentityService currencyIdentityService,
            @NotNull Duration transactionConfirmationTimeout
    ) {
        this.solanaBlockChain = Objects.requireNonNull(
                solanaBlockChain,
                "solanaBlockChain"
        );
        this.wallet = Objects.requireNonNull(wallet, "wallet");
        this.currencyIdentityService = Objects.requireNonNull(
                currencyIdentityService,
                "currencyIdentityService"
        );
        this.transactionConfirmationTimeout = requirePositiveTimeout(
                transactionConfirmationTimeout
        );
        this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
        this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
    }

    @Override
    public @Nullable JupiterPerpsPosition getPosition(@NotNull ΩJupiterPerpsPositionAccountΩ positionAccount)
            throws IOException, InterruptedException {
        SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionAccount);

        if (accountInfo == null) {
            throw new IllegalArgumentException("Jupiter Perps position account does not exist: " + positionAccount);
        }

        if (!JUPITER_PERPS_PROGRAM_ID.equals(accountInfo.owner())) {
            throw new IllegalArgumentException(
                    "Account is not owned by Jupiter Perps program: " + positionAccount
            );
        }

        JupiterPerpsPositionInfo perpsPositionInfo = positionDecoder.decode(accountInfo);

        if (perpsPositionInfo == null) {
            return null;
        }

        JupiterPerpsPosition pos = buildPosition(
                positionAccount,
                perpsPositionInfo
        );

        return pos;
    }

    @Override
    public @NotNull Set<JupiterPerpsPosition> getOpenPositions(@NotNull ΩSolanaWalletIdΩ owner)
            throws IOException, InterruptedException {
        Set<SolanaAccountInfo> accountInfos = solanaBlockChain.getProgramAccounts(
                JUPITER_PERPS_PROGRAM_ID,
                Set.of(new SolanaProgramAccountMemcmpFilter(
                        POSITION_OWNER_OFFSET,
                        owner
                ))
        );

        Set<JupiterPerpsPosition> positions = new HashSet<>();

        for (SolanaAccountInfo accountInfo : accountInfos) {
            System.out.println("      Found " + accountInfos.size() + " postion candidates...");
            ΩSolanaProgramIdΩ programId = accountInfo.owner();
            ΩJupiterPerpsPositionAccountΩ positionAccount = accountInfo.address();

            if (!JUPITER_PERPS_PROGRAM_ID.equals(programId)) {
                String errorMsg = "Account '" + positionAccount + "' is not owned by Jupiter Perps program '" +
                        programId + "'";
                throw new IllegalArgumentException(errorMsg);
            }

            JupiterPerpsPositionInfo perpsPositionInfo = positionDecoder.decode(accountInfo);
            if (perpsPositionInfo == null || perpsPositionInfo.positionSize().compareTo(ZERO) == 0) {
                continue;
            }

            JupiterPerpsPosition pos = buildPosition(
                    positionAccount,
                    perpsPositionInfo
            );

            positions.add(pos);
        }

        return Set.copyOf(positions);
    }

    @Override
    public @NotNull ΩSolanaTransactionSignatureΩ executePositionIncrease(
            @NotNull ΩSPLMintAddressΩ tradedTokenMint,
            @NotNull JupiterPerpsPositionDirection direction,
            @NotNull ΩUSDCAmountΩ inputTokenAmount,
            @NotNull ΩUSDCAmountΩ sizeUsdDelta,
            int maxSlippageBps
    ) throws IOException, JupiterTransactionOutcomeException,
            InterruptedException {
        SolanaUnsignedTransaction unsignedTransaction =
                buildPositionIncreaseTransaction(
                        wallet.getAddress(),
                        tradedTokenMint,
                        direction,
                        inputTokenAmount,
                        sizeUsdDelta,
                        maxSlippageBps
                );

        SolanaSignedTransaction signedTransaction =
                wallet.signTransaction(unsignedTransaction);

        ΩSolanaTransactionSignatureΩ transactionSignature =
                executePositionIncreaseTransaction(signedTransaction);
        awaitConfirmed("Jupiter Perps position increase", transactionSignature);
        return transactionSignature;
    }

    private @NotNull SolanaUnsignedTransaction buildPositionIncreaseTransaction(
            @NotNull ΩSolanaWalletIdΩ owner,
            @NotNull ΩSPLMintAddressΩ tradedTokenMint,
            @NotNull JupiterPerpsPositionDirection direction,
            @NotNull ΩUSDCAmountΩ inputTokenAmount,
            @NotNull ΩUSDCAmountΩ sizeUsdDelta,
            int maxSlippageBps
    ) throws IOException, InterruptedException {
        IncreasePositionRequest request = buildIncreasePositionRequest(
                owner,
                tradedTokenMint,
                direction,
                inputTokenAmount,
                sizeUsdDelta,
                maxSlippageBps
        );

        IncreasePositionResponse response = requestPositionIncrease(request);
        TransactionMetadata metadata = response.txMetadata();

        if (response.serializedTxBase64() == null
                || response.serializedTxBase64().isBlank()) {
            throw new IOException(
                    "Jupiter Perps response did not contain a serialized transaction"
            );
        }

        if (metadata == null) {
            throw new IOException(
                    "Jupiter Perps response did not contain transaction metadata"
            );
        }

        try {
            SolanaUnsignedTransaction utx = new SolanaUnsignedTransaction(
                    response.serializedTxBase64(),
                    metadata.blockhash(),
                    Long.parseLong(metadata.lastValidBlockHeight())
            );

            return utx;
        } catch (NumberFormatException e) {
            throw new IOException(
                    "Invalid lastValidBlockHeight returned by Jupiter: "
                            + metadata.lastValidBlockHeight(),
                    e
            );
        }
    }

    private @NotNull ΩSolanaTransactionSignatureΩ
    executePositionIncreaseTransaction(
            @NotNull SolanaSignedTransaction transaction
    ) throws IOException, InterruptedException {
        if (transaction.serializedTransaction() == null
                || transaction.serializedTransaction().isBlank()) {
            throw new IllegalArgumentException(
                    "Signed transaction must not be blank"
            );
        }

        ExecuteTransactionRequest request =
                new ExecuteTransactionRequest(
                        "increase-position",
                        transaction.serializedTransaction()
                );

        ExecuteTransactionResponse response =
                requestTransactionExecution(request);

        if (response.txid() == null || response.txid().isBlank()) {
            throw new IOException(
                    "Jupiter transaction execution response did not "
                            + "contain a transaction signature"
            );
        }

        return response.txid();
    }

    @Override
    public @NotNull ΩSolanaTransactionSignatureΩ executePositionDecrease(
            @NotNull ΩJupiterPerpsPositionAccountΩ positionAccount,
            @NotNull ΩSPLMintAddressΩ receiveTokenMint,
            @NotNull ΩUSDCAmountΩ sizeUsdDelta,
            int maxSlippageBps
    ) throws IOException, JupiterTransactionOutcomeException,
            InterruptedException {
        SolanaUnsignedTransaction unsignedTransaction =
                buildPositionDecreaseTransaction(
                        positionAccount,
                        receiveTokenMint,
                        sizeUsdDelta,
                        maxSlippageBps
                );

        SolanaSignedTransaction signedTransaction =
                wallet.signTransaction(unsignedTransaction);

        ΩSolanaTransactionSignatureΩ transactionSignature =
                executePositionDecreaseTransaction(signedTransaction);
        awaitConfirmed("Jupiter Perps position decrease", transactionSignature);
        return transactionSignature;
    }

    private void awaitConfirmed(
            String operation,
            ΩSolanaTransactionSignatureΩ transactionSignature
    ) throws IOException, JupiterTransactionOutcomeException,
            InterruptedException {
        SolanaTransactionOutcome outcome;
        try {
            outcome = solanaBlockChain.awaitTransaction(
                    transactionSignature,
                    SolanaCommitment.CONFIRMED,
                    transactionConfirmationTimeout
            );
        } catch (IOException e) {
            throw new IOException(
                    operation
                            + " confirmation failed for transaction "
                            + transactionSignature
                            + "; the on-chain outcome is unknown",
                    e
            );
        }

        switch (outcome.status()) {
            case SUCCEEDED -> {
                return;
            }
            case FAILED, TIMED_OUT -> throw
                    new JupiterTransactionOutcomeException(
                            transactionSignature,
                            SolanaCommitment.CONFIRMED,
                            outcome
                    );
        }
    }

    private static Duration requirePositiveTimeout(Duration timeout) {
        Objects.requireNonNull(timeout, "transactionConfirmationTimeout");
        if (timeout.isZero() || timeout.isNegative()) {
            throw new IllegalArgumentException(
                    "Transaction confirmation timeout must be greater than zero"
            );
        }
        return timeout;
    }

    private @NotNull SolanaUnsignedTransaction buildPositionDecreaseTransaction(
            @NotNull ΩJupiterPerpsPositionAccountΩ positionAccount,
            @NotNull ΩSPLMintAddressΩ receiveTokenMint,
            @NotNull ΩUSDCAmountΩ sizeUsdDelta,
            int maxSlippageBps
    ) throws IOException, InterruptedException {
        DecreasePositionRequest request = buildDecreasePositionRequest(
                positionAccount,
                receiveTokenMint,
                sizeUsdDelta,
                maxSlippageBps
        );

        DecreasePositionResponse response =
                requestPositionDecrease(request);

        TransactionMetadata metadata = response.txMetadata();

        if (response.serializedTxBase64() == null
                || response.serializedTxBase64().isBlank()) {
            throw new IOException(
                    "Jupiter Perps response did not contain a serialized transaction"
            );
        }

        if (metadata == null) {
            throw new IOException(
                    "Jupiter Perps response did not contain transaction metadata"
            );
        }

        try {
            return new SolanaUnsignedTransaction(
                    response.serializedTxBase64(),
                    metadata.blockhash(),
                    Long.parseLong(metadata.lastValidBlockHeight())
            );
        } catch (NumberFormatException e) {
            throw new IOException(
                    "Invalid lastValidBlockHeight returned by Jupiter: "
                            + metadata.lastValidBlockHeight(),
                    e
            );
        }
    }

    private @NotNull ΩSolanaTransactionSignatureΩ
    executePositionDecreaseTransaction(
            @NotNull SolanaSignedTransaction transaction
    ) throws IOException, InterruptedException {
        if (transaction.serializedTransaction() == null
                || transaction.serializedTransaction().isBlank()) {
            throw new IllegalArgumentException(
                    "Signed transaction must not be blank"
            );
        }

        ExecuteTransactionRequest request =
                new ExecuteTransactionRequest(
                        "decrease-position",
                        transaction.serializedTransaction()
                );

        ExecuteTransactionResponse response =
                requestTransactionExecution(request);

        if (response.txid() == null || response.txid().isBlank()) {
            throw new IOException(
                    "Jupiter transaction execution response did not "
                            + "contain a transaction signature"
            );
        }

        return response.txid();
    }

    private static DecreasePositionRequest buildDecreasePositionRequest(
            ΩJupiterPerpsPositionAccountΩ positionAccount,
            ΩSPLMintAddressΩ receiveTokenMint,
            ΩUSDCAmountΩ sizeUsdDelta,
            int maxSlippageBps
    ) {
        if (positionAccount.isBlank()) {
            throw new IllegalArgumentException(
                    "Position account must not be blank"
            );
        }

        if (sizeUsdDelta.signum() <= 0) {
            throw new IllegalArgumentException(
                    "Position size delta must be greater than zero: "
                            + sizeUsdDelta
            );
        }

        if (maxSlippageBps < 0 || maxSlippageBps > BASIS_POINTS_DIVISOR) {
            throw new IllegalArgumentException(
                    "Maximum slippage must be between 0 and "
                            + BASIS_POINTS_DIVISOR
                            + " basis points: "
                            + maxSlippageBps
            );
        }

        return new DecreasePositionRequest(
                positionAccount,
                toProtocolReceiveToken(receiveTokenMint),
                toMicroAmount(sizeUsdDelta),
                Integer.toString(maxSlippageBps)
        );
    }

    private JupiterPerpsPosition buildPosition(
            ΩJupiterPerpsPositionAccountΩ positionAccount,
            JupiterPerpsPositionInfo perpsPositionInfo
    ) throws IOException, InterruptedException {
        ΩJupiterPerpsCustodyAccountAddressΩ custodyAccountAddress = perpsPositionInfo.custodyAccountAddress();

        System.out.print("      Getting account info for: " + custodyAccountAddress + "...");
        ΩJupiterCustodyAccountInfoEncodedΩ encodedCustodyAccountInfo = solanaBlockChain.getAccountInfo(custodyAccountAddress);
        System.out.println(" Done.");

        if (encodedCustodyAccountInfo == null) {
            String errorMsg = "Jupiter Perps custody account does not exist: " + custodyAccountAddress;
            throw new IllegalArgumentException(errorMsg);
        }

        JupiterCustodyAccountInfo custodyAccountInfo = custodyDecoder.decode(encodedCustodyAccountInfo);

        ΩSPLMintAddressΩ mintAddress = custodyAccountInfo.mintAddress();

        ΩJupiterPerpsCollateralCustodyAccountAddressΩ collateralCustodyAccountAddress =
                perpsPositionInfo.collateralCustodyAccountAddress();

        System.out.print("      Getting collateral custody account Info for: " + custodyAccountAddress + "...");
        ΩJupiterCustodyAccountInfoEncodedΩ encodedCollateralCustodyAccountInfo =
                solanaBlockChain.getAccountInfo(collateralCustodyAccountAddress);
        System.out.println(" Done.");

        if (encodedCollateralCustodyAccountInfo == null) {
            String errorMsg = "Jupiter Perps collateral custody account does not exist: " + collateralCustodyAccountAddress;
            throw new IllegalArgumentException(errorMsg);
        }

        System.out.print("      Calculating fiels in position...");
        JupiterCustodyAccountInfo collateralCustodyAccountInfo =
                custodyDecoder.decode(encodedCollateralCustodyAccountInfo);

        ΩJupiterPostionInterestΩ positionInterest = collateralCustodyAccountInfo.currentCumulativeInterestRate()
                .subtract(perpsPositionInfo.cumulativeInterestSnapshot());

        ΩUSDCAmountΩ positionSize = perpsPositionInfo.positionSize();
        ΩUSDCAmountΩ borrowFeesDue = calculateBorrowFeesDue(positionInterest, positionSize);
        ΩUSDCAmountΩ closeFeePending = calculateCloseFeePending(positionSize);
        ΩUSDCPriceΩ liquidationPrice = calculateLiquidationPrice(
                perpsPositionInfo.direction(),
                perpsPositionInfo.entryPrice(),
                perpsPositionInfo.collateral(),
                closeFeePending,
                borrowFeesDue,
                positionSize
        );

        ΩUSDCAmountΩ value = ZERO;                       // TODO - Dummy
        ΩUSDCAmountΩ pnl = ZERO;                         // TODO - Dummy
        BigDecimal pnlPercent = ZERO;                    // TODO - Dummy
        BigDecimal leverage = ZERO;                      // TODO - Dummy
        ΩUSDCPriceΩ marketPrice = ZERO;                  // TODO - Dummy
        ΩUSDCAmountΩ totalFees = ZERO;                   // TODO - Dummy
        ΩSolanaAmountΩ accountRent = new MoneyAmount(    // TODO - Dummy
                ZERO,
                currencyIdentityService.resolve(SOLANA_ID)
        );

        JupiterPerpsPosition pos = new JupiterPerpsPosition(
                positionAccount,
                mintAddress,
                perpsPositionInfo.direction(),
                value,
                positionSize,
                pnl,
                pnlPercent,
                leverage,
                perpsPositionInfo.entryPrice(),
                liquidationPrice,
                marketPrice,
                perpsPositionInfo.collateral(),
                totalFees,
                borrowFeesDue,
                closeFeePending,
                accountRent
        );

        System.out.println(" Done.");
        return pos;
    }

    private static ΩUSDCAmountΩ calculateBorrowFeesDue(
            ΩJupiterPostionInterestΩ positionInterest,
            ΩUSDCAmountΩ size
    ) {
        ΩUSDCAmountΩ fees = new BigDecimal(positionInterest)
                .multiply(size)
                .divide(
                        BigDecimal.valueOf(RATE_POWER),
                        USDC_DECIMALS,
                        RoundingMode.CEILING
                );
        return fees;
    }

    private static ΩUSDCAmountΩ calculateCloseFeePending(ΩUSDCAmountΩ size) {
        ΩUSDCAmountΩ fee = size
                .multiply(BigDecimal.valueOf(CLOSE_FEE_BPS))
                .divide(
                        BigDecimal.valueOf(BASIS_POINTS_DIVISOR),
                        USDC_DECIMALS,
                        RoundingMode.CEILING
                );

        return fee;
    }

    private static ΩUSDCPriceΩ calculateLiquidationPrice(
            JupiterPerpsPositionDirection direction,
            ΩUSDCPriceΩ price,
            ΩUSDCAmountΩ collateral,
            ΩUSDCAmountΩ closeFeePending,
            ΩUSDCAmountΩ borrowFeesDue,
            ΩUSDCAmountΩ positionSize
    ) {
        ΩUSDCAmountΩ minimumCollateral = positionSize.divide(
                BigDecimal.valueOf(MAX_LEVERAGE),
                USDC_DECIMALS,
                RoundingMode.CEILING
        );

        ΩUSDCAmountΩ maxLoss = minimumCollateral
                .add(closeFeePending)
                .add(borrowFeesDue);

        ΩUSDCAmountΩ maxPriceDiffAmount = maxLoss
                .subtract(collateral)
                .abs();

        ΩUSDCPriceΩ maxPriceDiff = maxPriceDiffAmount
                .multiply(price)
                .divide(
                        positionSize,
                        USDC_DECIMALS,
                        RoundingMode.CEILING
                );

        boolean maxLossExceedsCollateral = maxLoss.compareTo(collateral) > 0;

        if (direction == JupiterPerpsPositionDirection.LONG) {
            if (maxLossExceedsCollateral) {
                return price.add(maxPriceDiff);
            }

            return price.subtract(maxPriceDiff);
        }

        if (direction == JupiterPerpsPositionDirection.SHORT) {
            if (maxLossExceedsCollateral) {
                return price.subtract(maxPriceDiff);
            }

            return price.add(maxPriceDiff);
        }

        throw new IllegalArgumentException("Unsupported Jupiter Perps position direction: " + direction);
    }

    private static IncreasePositionRequest buildIncreasePositionRequest(
            ΩSolanaWalletIdΩ owner,
            ΩSPLMintAddressΩ tradedTokenMint,
            JupiterPerpsPositionDirection direction,
            ΩUSDCAmountΩ inputTokenAmount,
            ΩUSDCAmountΩ sizeUsdDelta,
            int maxSlippageBps
    ) {
        if (inputTokenAmount.signum() <= 0) {
            throw new IllegalArgumentException(
                    "Input token amount must be greater than zero: " + inputTokenAmount
            );
        }

        if (sizeUsdDelta.signum() < 0) {
            throw new IllegalArgumentException(
                    "Position size delta must not be negative: " + sizeUsdDelta
            );
        }

        if (maxSlippageBps < 0 || maxSlippageBps > BASIS_POINTS_DIVISOR) {
            throw new IllegalArgumentException(
                    "Maximum slippage must be between 0 and "
                            + BASIS_POINTS_DIVISOR
                            + " basis points: "
                            + maxSlippageBps
            );
        }

        IncreasePositionRequest request = new IncreasePositionRequest(
                toProtocolAsset(tradedTokenMint),
                "USDC",
                toMicroAmount(inputTokenAmount),
                toProtocolSide(direction),
                Integer.toString(maxSlippageBps),
                toMicroAmount(sizeUsdDelta),
                owner
        );

        return request;
    }

    private static String toProtocolAsset(ΩSPLMintAddressΩ tradedTokenMint) {
        if (SOL_MINT.equals(tradedTokenMint)) {
            return "SOL";
        }

        if (BTC_MINT.equals(tradedTokenMint)) {
            return "BTC";
        }

        if (ETH_MINT.equals(tradedTokenMint)) {
            return "ETH";
        }

        throw new IllegalArgumentException(
                "Unsupported Jupiter Perps traded-token mint: " + tradedTokenMint
        );
    }

    private static String toProtocolReceiveToken(
            ΩSPLMintAddressΩ receiveTokenMint
    ) {
        if (SOL_MINT.equals(receiveTokenMint)) {
            return "SOL";
        }

        if (BTC_MINT.equals(receiveTokenMint)) {
            return "BTC";
        }

        if (ETH_MINT.equals(receiveTokenMint)) {
            return "ETH";
        }

        if (USDC_MINT.equals(receiveTokenMint)) {
            return "USDC";
        }

        throw new IllegalArgumentException(
                "Unsupported Jupiter Perps receive-token mint: "
                        + receiveTokenMint
        );
    }

    private static String toProtocolSide(
            JupiterPerpsPositionDirection direction
    ) {
        return switch (direction) {
            case LONG -> "long";
            case SHORT -> "short";
        };
    }

    private static String toMicroAmount(BigDecimal amount) {
        return amount
                .movePointRight(USDC_DECIMALS)
                .toBigIntegerExact()
                .toString();
    }

    private static IncreasePositionResponse requestPositionIncrease(
            IncreasePositionRequest request
    ) throws IOException, InterruptedException {
        String requestJson = OBJECT_MAPPER.writeValueAsString(request);

        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(INCREASE_POSITION_ENDPOINT)
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .header("x-client-platform", "nenjim-hub")
                .POST(HttpRequest.BodyPublishers.ofString(requestJson))
                .build();

        HttpResponse<String> httpResponse = HTTP_CLIENT.send(
                httpRequest,
                HttpResponse.BodyHandlers.ofString()
        );

        if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {
            throw new IOException(
                    "Jupiter Perps increase-position request failed with HTTP "
                            + httpResponse.statusCode()
                            + ": "
                            + httpResponse.body()
            );
        }

        IncreasePositionResponse response = OBJECT_MAPPER.readValue(
                httpResponse.body(),
                IncreasePositionResponse.class
        );

        TransactionMetadata metadata = response.txMetadata();

        if (metadata.blockhash() == null || metadata.blockhash().isBlank()) {
            String errorMsg = "Jupiter Perps response did not contain a blockhash";
            throw new IOException(errorMsg);
        }

        long lastValidBlockHeight;

        try {
            lastValidBlockHeight =
                    Long.parseLong(metadata.lastValidBlockHeight());
        } catch (NumberFormatException e) {
            String errorMsg = "Invalid lastValidBlockHeight returned by Jupiter: " + metadata.lastValidBlockHeight();
            throw new IOException(errorMsg, e);
        }

        if (lastValidBlockHeight <= 0) {
            String errorMsg = "Invalid lastValidBlockHeight returned by Jupiter: " + lastValidBlockHeight;
            throw new IOException(errorMsg);
        }

        return response;
    }

    private static DecreasePositionResponse requestPositionDecrease(
            DecreasePositionRequest request
    ) throws IOException, InterruptedException {
        String requestJson = OBJECT_MAPPER.writeValueAsString(request);

        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(DECREASE_POSITION_ENDPOINT)
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .header("x-client-platform", "nenjim-hub")
                .POST(HttpRequest.BodyPublishers.ofString(requestJson))
                .build();

        HttpResponse<String> httpResponse = HTTP_CLIENT.send(
                httpRequest,
                HttpResponse.BodyHandlers.ofString()
        );

        if (httpResponse.statusCode() < 200
                || httpResponse.statusCode() >= 300) {
            throw new IOException(
                    "Jupiter Perps decrease-position request failed with HTTP "
                            + httpResponse.statusCode()
                            + ": "
                            + httpResponse.body()
            );
        }

        DecreasePositionResponse response = OBJECT_MAPPER.readValue(
                httpResponse.body(),
                DecreasePositionResponse.class
        );

        TransactionMetadata metadata = response.txMetadata();

        if (metadata == null) {
            throw new IOException(
                    "Jupiter Perps response did not contain transaction metadata"
            );
        }

        if (metadata.blockhash() == null || metadata.blockhash().isBlank()) {
            throw new IOException(
                    "Jupiter Perps response did not contain a blockhash"
            );
        }

        long lastValidBlockHeight;

        try {
            lastValidBlockHeight =
                    Long.parseLong(metadata.lastValidBlockHeight());
        } catch (NumberFormatException e) {
            throw new IOException(
                    "Invalid lastValidBlockHeight returned by Jupiter: "
                            + metadata.lastValidBlockHeight(),
                    e
            );
        }

        if (lastValidBlockHeight <= 0) {
            throw new IOException(
                    "Invalid lastValidBlockHeight returned by Jupiter: "
                            + lastValidBlockHeight
            );
        }

        return response;
    }

    private static ExecuteTransactionResponse requestTransactionExecution(
            ExecuteTransactionRequest request
    ) throws IOException, InterruptedException {
        String requestJson =
                OBJECT_MAPPER.writeValueAsString(request);

        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(EXECUTE_TRANSACTION_ENDPOINT)
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .header("x-client-platform", "nenjim-hub")
                .POST(HttpRequest.BodyPublishers.ofString(requestJson))
                .build();

        HttpResponse<String> httpResponse = HTTP_CLIENT.send(
                httpRequest,
                HttpResponse.BodyHandlers.ofString()
        );

        if (httpResponse.statusCode() < 200
                || httpResponse.statusCode() >= 300) {
            throw new IOException(
                    "Jupiter transaction execution failed with HTTP "
                            + httpResponse.statusCode()
                            + ": "
                            + httpResponse.body()
            );
        }

        return OBJECT_MAPPER.readValue(
                httpResponse.body(),
                ExecuteTransactionResponse.class
        );
    }

    private static final ΩSPLMintAddressΩ BTC_MINT  = "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh";
    private static final ΩSPLMintAddressΩ ETH_MINT  = "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs";
    private static final ΩSPLMintAddressΩ SOL_MINT  = "So11111111111111111111111111111111111111112";
    private static final ΩSPLMintAddressΩ USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

    private static final URI EXECUTE_TRANSACTION_ENDPOINT =
            URI.create(
                    "https://perps-api.jup.ag/v2/transaction/execute"
            );

    private static final URI DECREASE_POSITION_ENDPOINT =
            URI.create(
                    "https://perps-api.jup.ag/v2/positions/decrease"
            );

    private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";

    private static final URI INCREASE_POSITION_ENDPOINT = URI.create("https://perps-api.jup.ag/v2/positions/increase");
    private static final Duration DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT =
            Duration.ofMinutes(2);
    private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final long RATE_POWER = 1_000_000_000L;
    private static final int CLOSE_FEE_BPS = 6;
    private static final int BASIS_POINTS_DIVISOR = 10_000;
    private static final int POSITION_OWNER_OFFSET = 8;
    private static final int MAX_LEVERAGE = 500;
    private static final int USDC_DECIMALS = 6;
    private final SolanaBlockChain solanaBlockChain;
    private final SolanaWallet wallet;
    private final CurrencyIdentityService currencyIdentityService;
    private final Duration transactionConfirmationTimeout;
    private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
    private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
}
