using System.Numerics;
using Managing.ABI.GmxV2.DataStore;
using Managing.ABI.GmxV2.DataStore.ContractDefinition;
using Managing.ABI.GmxV2.ExchangeRouter;
using Managing.ABI.GmxV2.ExchangeRouter.ContractDefinition;
using Managing.ABI.GmxV2.Multicall3.ContractDefinition;
using Managing.ABI.GmxV2.Reader;
using Managing.ABI.GmxV2.Reader.ContractDefinition;
using Managing.Common;
using Managing.Core;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Models.Gmx.v2;
using Managing.Infrastructure.Evm.Referentials;
using Nethereum.Contracts;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using Nethereum.Web3;
using GetAccountOrdersFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetAccountOrdersFunction;
using GetAccountOrdersOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetAccountOrdersOutputDTO;
using GetAccountPositionInfoListFunction =
    Managing.ABI.GmxV2.SyntheticsReader.ContractDefinition.GetAccountPositionInfoListFunction;
using GetAccountPositionInfoListOutputDTO =
    Managing.ABI.GmxV2.SyntheticsReader.ContractDefinition.GetAccountPositionInfoListOutputDTO;
using GetMarketInfoFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketInfoFunction;
using GetMarketInfoOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketInfoOutputDTO;
using GetMarketsFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketsFunction;
using GetMarketsOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketsOutputDTO;
using GetMarketTokenPriceFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketTokenPriceFunction;
using GetMarketTokenPriceOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketTokenPriceOutputDTO;
using MarketPrices = Managing.ABI.GmxV2.Reader.ContractDefinition.MarketPrices;

namespace Managing.Infrastructure.Evm.Services.Gmx;

public class GmxV2Service
{
    private readonly GmxV2Encoder _encoder = new GmxV2Encoder();
    private readonly BigInteger _defaultCount = new BigInteger(1000);
    private readonly OracleKeeperService _oracleKeeperService = new OracleKeeperService();

    public GmxV2Service()
    {
    }

    public async Task<object> GetMarkets(Web3 web3)
    {
        var reader = new ReaderService(web3, Arbitrum.AddressV2.Reader);
        var result = await reader.GetMarketsQueryAsync(Arbitrum.AddressV2.DataStore, 0, 100);
        return result;
    }

    public async Task<List<GmxMarket>> GetMarketsAsync(Web3 web3)
    {
        var getMarketCallData = new GetMarketsFunction
        {
            DataStore = Arbitrum.AddressV2.DataStore,
            Start = 0,
            End = 10
        }.GetCallData();

        var call1 = new Call
        {
            Target = Arbitrum.AddressV2.Reader,
            CallData = getMarketCallData
        };

        var aggregateFunction = new AggregateFunction();
        aggregateFunction.Calls = new List<Call>();
        aggregateFunction.Calls.Add(call1);

        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var returnCalls = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        var markets = new GetMarketsOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex());
        var result = new List<GmxMarket>();
        foreach (var market in markets.ReturnValue1)
        {
            try
            {
                result.Add(GmxMappers.MapInfos(market));
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        return result;
    }

    public async Task<GmxMarketInfo> GetMarketInfo(Web3 web3)
    {
        var reader = new ReaderService(web3, Arbitrum.AddressV2.Reader);
        var market = (await GetMarketsAsync(web3)).First();
        var tokensData = await GetTokensData(web3);
        var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market);
        var response =
            await reader.GetMarketInfoQueryAsync(Arbitrum.AddressV2.DataStore, marketPrices, market.MarketToken);

        // Get hashed key to call datastore
        var longInterestUsingLongTokenCallData = new GetUintFunction()
        {
            Key = _encoder.HashData([
                ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                ("address", market.MarketToken),
                ("address", market.LongToken),
                ("bool", true)
            ])
        }.GetCallData();
        var longInterestUsingLongTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = longInterestUsingLongTokenCallData
        };
        var longInterestUsingShortTokenCallData = new GetUintFunction()
        {
            Key = _encoder.HashData([
                ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                ("address", market.MarketToken),
                ("address", market.ShortToken),
                ("bool", true)
            ])
        }.GetCallData();
        var longInterestUsingShortTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = longInterestUsingShortTokenCallData
        };
        var shortInterestUsingLongTokenCallData = new GetUintFunction()
        {
            Key = _encoder.HashData([
                ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                ("address", market.MarketToken),
                ("address", market.LongToken),
                ("bool", false)
            ])
        }.GetCallData();
        var shortInterestUsingLongTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = shortInterestUsingLongTokenCallData
        };
        var shortInterestUsingShortTokenCallData = new GetUintFunction()
        {
            Key = _encoder.HashData([
                ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                ("address", market.MarketToken),
                ("address", market.ShortToken),
                ("bool", false)
            ])
        }.GetCallData();
        var shortInterestUsingShortTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = shortInterestUsingShortTokenCallData
        };

        var dataStoreMulticall = new AggregateFunction();
        dataStoreMulticall.Calls = new List<Call>();
        dataStoreMulticall.Calls.Add(longInterestUsingLongTokenCall);
        dataStoreMulticall.Calls.Add(longInterestUsingShortTokenCall);
        dataStoreMulticall.Calls.Add(shortInterestUsingLongTokenCall);
        dataStoreMulticall.Calls.Add(shortInterestUsingShortTokenCall);
        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var returnCalls = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(dataStoreMulticall, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        var marketInfos = GmxMappers.Map(response.ReturnValue1);
        var result = new GmxMarketInfo()
        {
            Market = marketInfos,
            Infos = DecodeFundingRateOutput(returnCalls, marketInfos.IsSameCollaterals)
        };

        return result;
    }

    public async Task<GmxMarketTokenPrice> GetMarketTokenPrice(Web3 web3, bool maximize = true)
    {
        var reader = new ReaderService(web3, Arbitrum.AddressV2.Reader);
        var market = (await GetMarketsAsync(web3)).First();
        var tokensData = await GetTokensData(web3);
        var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market);
        var marketProps = new MarketsProps()
        {
            MarketToken = market.MarketToken,
            IndexToken = market.IndexToken,
            LongToken = market.LongToken,
            ShortToken = market.ShortToken
        };

        var response = await reader.GetMarketTokenPriceQueryAsync(Arbitrum.AddressV2.DataStore, marketProps,
            marketPrices.IndexTokenPrice, marketPrices.LongTokenPrice, marketPrices.ShortTokenPrice,
            _encoder.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS), maximize);

        return GmxMappers.Map(response);
    }

    public async Task<bool> GetIsMarketDisabled(Web3 web3)
    {
        var dataStoreService = new DataStoreService(web3, Arbitrum.AddressV2.DataStore);
        var market = (await GetMarketsAsync(web3)).First();
        var hashString = _encoder.HashString(Constants.GMX.MARKET_DISABLED_KEY);

        var parameters = new List<(string dataType, object dataValues)>
        {
            ("bytes32", hashString),
            ("address", market.MarketToken)
        };
        var response = await dataStoreService.GetBoolQueryAsync(_encoder.HashData(parameters));
        return response;
    }

    public async Task<BigInteger> GetLongInterestAmount(Web3 web3)
    {
        var dataStoreService = new DataStoreService(web3, Arbitrum.AddressV2.DataStore);
        var market = (await GetMarketsAsync(web3)).First();
        var parameters = new List<(string dataType, object dataValues)>
        {
            ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
            ("address", market.MarketToken),
            ("address", market.LongToken),
            ("bool", true)
        };
        var response = await dataStoreService.GetUintQueryAsync(_encoder.HashData(parameters));
        return response;
    }

    public async Task<List<GmxMarketInfo>> GetMarketInfosAsync(Web3 web3)
    {
        var markets = await GetMarketsAsync(web3);
        var readerResult = new List<GmxMarketInfo>();
        var readerCalls = new List<Call>();
        var datastoreCalls = new List<Call>();
        markets = markets.Where(m => !m.IsSpotOnly).ToList();
        var tokensData = await GetTokensData(web3);

        foreach (var market in markets)
        {
            var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market);
            var marketProps = new MarketsProps()
            {
                MarketToken = market.MarketToken,
                IndexToken = market.IndexToken,
                LongToken = market.LongToken,
                ShortToken = market.ShortToken
            };
            var getMarketInfoCallData = new GetMarketInfoFunction
            {
                DataStore = Arbitrum.AddressV2.DataStore,
                Prices = marketPrices,
                MarketKey = market.MarketToken
            }.GetCallData();
            var getMarketInfoCall = new Call
            {
                Target = Arbitrum.AddressV2.Reader,
                CallData = getMarketInfoCallData
            };
            var getMarketTokenPriceCallData = new GetMarketTokenPriceFunction
            {
                DataStore = Arbitrum.AddressV2.DataStore,
                Market = marketProps,
                IndexTokenPrice = marketPrices.IndexTokenPrice,
                LongTokenPrice = marketPrices.LongTokenPrice,
                ShortTokenPrice = marketPrices.ShortTokenPrice,
                PnlFactorType = _encoder.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS),
                Maximize = true
            }.GetCallData();
            var getMarketTokenPriceCall = new Call
            {
                Target = Arbitrum.AddressV2.Reader,
                CallData = getMarketTokenPriceCallData
            };
            var getMarketTokenPriceMinCallData = new GetMarketTokenPriceFunction
            {
                DataStore = Arbitrum.AddressV2.DataStore,
                Market = marketProps,
                IndexTokenPrice = marketPrices.IndexTokenPrice,
                LongTokenPrice = marketPrices.LongTokenPrice,
                ShortTokenPrice = marketPrices.ShortTokenPrice,
                PnlFactorType = _encoder.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS),
                Maximize = false
            }.GetCallData();
            var getMarketTokenPriceMinCall = new Call
            {
                Target = Arbitrum.AddressV2.Reader,
                CallData = getMarketTokenPriceMinCallData
            };

            readerCalls.Add(getMarketInfoCall);
            readerCalls.Add(getMarketTokenPriceCall);
            readerCalls.Add(getMarketTokenPriceMinCall);

            var longInterestUsingLongTokenCallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                    ("address", market.MarketToken),
                    ("address", market.LongToken),
                    ("bool", true)
                ])
            }.GetCallData();
            var longInterestUsingLongTokenCall = new Call
            {
                Target = Arbitrum.AddressV2.DataStore,
                CallData = longInterestUsingLongTokenCallData
            };
            var longInterestUsingShortTokenCallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                    ("address", market.MarketToken),
                    ("address", market.ShortToken),
                    ("bool", true)
                ])
            }.GetCallData();
            var longInterestUsingShortTokenCall = new Call
            {
                Target = Arbitrum.AddressV2.DataStore,
                CallData = longInterestUsingShortTokenCallData
            };
            var shortInterestUsingLongTokenCallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                    ("address", market.MarketToken),
                    ("address", market.LongToken),
                    ("bool", false)
                ])
            }.GetCallData();
            var shortInterestUsingLongTokenCall = new Call
            {
                Target = Arbitrum.AddressV2.DataStore,
                CallData = shortInterestUsingLongTokenCallData
            };
            var shortInterestUsingShortTokenCallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)),
                    ("address", market.MarketToken),
                    ("address", market.ShortToken),
                    ("bool", false)
                ])
            }.GetCallData();
            var shortInterestUsingShortTokenCall = new Call
            {
                Target = Arbitrum.AddressV2.DataStore,
                CallData = shortInterestUsingShortTokenCallData
            };

            datastoreCalls.Add(longInterestUsingLongTokenCall);
            datastoreCalls.Add(longInterestUsingShortTokenCall);
            datastoreCalls.Add(shortInterestUsingLongTokenCall);
            datastoreCalls.Add(shortInterestUsingShortTokenCall);
        }

        var readerMulticall = new AggregateFunction { Calls = readerCalls };
        var datastoreMulticall = new AggregateFunction { Calls = datastoreCalls };

        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var readerCallResults = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(readerMulticall, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);
        var datastoreCallResults = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(datastoreMulticall, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        int readerCallIndex = 0;
        int datastoreCallIndex = 0;
        foreach (var market in markets)
        {
            var marketInfo =
                new GetMarketInfoOutputDTO().DecodeOutput(readerCallResults.ReturnData[readerCallIndex++].ToHex());
            var marketTokenPriceMax =
                new GetMarketTokenPriceOutputDTO().DecodeOutput(readerCallResults.ReturnData[readerCallIndex++]
                    .ToHex());
            var marketTokenPriceMin =
                new GetMarketTokenPriceOutputDTO().DecodeOutput(readerCallResults.ReturnData[readerCallIndex++]
                    .ToHex());

            var longInterestUsingLongToken = new GetUintOutputDTO()
                .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1;
            var longInterestUsingShortToken = new GetUintOutputDTO()
                .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1;
            var shortInterestUsingLongToken = new GetUintOutputDTO()
                .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1;
            var shortInterestUsingShortToken = new GetUintOutputDTO()
                .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1;

            var marketInfos = GmxMappers.Map(marketInfo.ReturnValue1);
            readerResult.Add(new GmxMarketInfo
            {
                Market = marketInfos,
                Infos = new GmxMarketInfos
                {
                    LongInterestUsd = longInterestUsingLongToken + longInterestUsingShortToken,
                    ShortInterestUsd = shortInterestUsingLongToken + shortInterestUsingShortToken
                },
                MarketTokenPriceMax = GmxMappers.Map(marketTokenPriceMax),
                MarketTokenPriceMin = GmxMappers.Map(marketTokenPriceMin)
            });
        }

        return readerResult;
    }

    private GmxMarketInfos DecodeFundingRateOutput(AggregateOutputDTO results, bool isSameCollaterals)
    {
        var marketDivisor = new BigInteger(isSameCollaterals ? 2 : 1);
        var longInterestUsingLongToken =
            new GetUintOutputDTO().DecodeOutput(results.ReturnData[0].ToHex()).ReturnValue1 / marketDivisor;
        var longInterestUsingShortToken =
            new GetUintOutputDTO().DecodeOutput(results.ReturnData[1].ToHex()).ReturnValue1 / marketDivisor;
        var shortInterestUsingLongToken =
            new GetUintOutputDTO().DecodeOutput(results.ReturnData[2].ToHex()).ReturnValue1 / marketDivisor;
        var shortInterestUsingShortToken =
            new GetUintOutputDTO().DecodeOutput(results.ReturnData[3].ToHex()).ReturnValue1 / marketDivisor;

        var longInterestUsd = longInterestUsingLongToken + longInterestUsingShortToken;
        var shortInterestUsd = shortInterestUsingLongToken + shortInterestUsingShortToken;

        var marketInfos = new GmxMarketInfos()
        {
            LongInterestUsd = longInterestUsd,
            ShortInterestUsd = shortInterestUsd
        };
        return marketInfos;
    }

    public async Task<List<GmxTokenData>> GetTokensData(Web3 web3, bool fastCall = false)
    {
        var tokens = TokenV2Service.GetTokens().ToList();
        var balances = fastCall ? null : await GetTokenBalances(web3, tokens);
        var prices = await GetTokenPrices(web3, tokens);
        var result = new List<GmxTokenData>();

        foreach (var token in tokens)
        {
            try
            {
                BigInteger? balance = null;
                if (balances?.Balances != null && balances.Balances.ContainsKey(token.Address))
                {
                    balance = balances.Balances[token.Address];
                }

                var price = prices[token.Address].Price;

                var data = new GmxTokenData()
                {
                    Address = token.Address,
                    Name = token.Name,
                    Symbol = token.Symbol,
                    Decimals = token.Decimals,
                    Price = price,
                    Balance = balance
                };
                result.Add(data);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        return result;
    }

    private async Task<Dictionary<string, GmxTokenPriceData>> GetTokenPrices(Web3 web3, List<GmxToken> tokens)
    {
        var result = new Dictionary<string, GmxTokenPriceData>();
        var tickersPrices = await _oracleKeeperService.GetTickers();
        foreach (var token in tokens)
        {
            var price = tickersPrices.FirstOrDefault(t => GmxV2Helpers.SameAddress(t.TokenAddress, token.Address));

            if (price == null && GmxV2Helpers.SameAddress(token.Address, Arbitrum.Address.Zero))
            {
                price = tickersPrices.FirstOrDefault(t =>
                    GmxV2Helpers.SameAddress(t.TokenAddress, Constants.GMX.TokenAddress.WETH));
            }

            result.Add(token.Address, new GmxTokenPriceData()
            {
                Price = new MarketPrice()
                {
                    Min = price != null ? BigInteger.Parse(price.MinPrice) : BigInteger.Zero,
                    Max = price != null ? BigInteger.Parse(price.MaxPrice) : BigInteger.Zero,
                },
                Balance = 0,
                TotalSupply = 0
            });
        }

        return result;
    }

    public async Task<GmxTokenBalances> GetTokenBalances(Web3 web3, List<GmxToken> tokens, string? account = null)
    {
        var result = new GmxTokenBalances();
        result.Balances = new Dictionary<string, BigInteger>();
        var aggregateFunction = new AggregateFunction();
        aggregateFunction.Calls = new List<Call>();
        var index = 0;
        foreach (var token in tokens)
        {
            if (token.IsNative)
            {
                var nativeBalanceCallData = new GetEthBalanceFunction()
                {
                    Addr = account ?? AddressUtil.ZERO_ADDRESS
                }.GetCallData();
                aggregateFunction.Calls.Add(new Call
                {
                    Target = Arbitrum.AddressV2.Multicall,
                    CallData = nativeBalanceCallData
                });
            }
            else
            {
                var balanceCallData = new BalanceOfFunction()
                {
                    Owner = account ?? GmxHelpers.GetRandomAddress()
                }.GetCallData();
                aggregateFunction.Calls.Add(new Call
                {
                    Target = token.Address,
                    CallData = balanceCallData
                });
            }
        }

        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var returnCalls = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        // var balances = new BalanceOfOutputDTOBase().DecodeOutput(returnCalls.ReturnData[0].ToHex());
        // result.Balances.Add(token.Address, balances.Balance);
        foreach (var callResult in returnCalls.ReturnData)
        {
            var balance = new BalanceOfOutputDTOBase().DecodeOutput(callResult.ToHex());
            result.Balances.Add(tokens[index].Address, balance.Balance);
            index++;
        }

        return result;
    }

    public async Task<List<FundingRate>> GetFundingRate(Web3 web3)
    {
        var market = await GetMarketInfo(web3);
        return GmxMappers.Map(market);
    }

    public async Task<List<FundingRate>> GetFundingRates(Web3 web3)
    {
        var fundingRates = new List<FundingRate>();
        var marketDatas = await GetMarketInfosAsync(web3);
        foreach (var gmxMarketInfo in marketDatas)
        {
            if (!Constants.GMX.Config.DeltaNeutralTickers.Contains(GmxHelpers.GetTicker(gmxMarketInfo.Market.Symbol)))
                continue;

            var rates = GmxMappers.Map(gmxMarketInfo);
            fundingRates.AddRange(rates);
        }

        return fundingRates;
    }

    public async Task<List<GmxV2Order>> GetOrders(Web3 web3, string publicAddress, Enums.Ticker ticker)
    {
        var marketAddress = GmxV2Helpers.GetMarketAddress(ticker);
        var orders = await GetAllOrders(web3, publicAddress);
        return orders.Where(o => o.MarketAddress == marketAddress).ToList();
    }

    private async Task<List<GmxV2Order>> GetAllOrders(Web3 web3, string publicAddress)
    {
        // Call Datastore and reader to construct open orders list 
        var aggregateFunction = new AggregateFunction();
        aggregateFunction.Calls = new List<Call>();

        var countCallData = new GetBytes32CountFunction
        {
            SetKey = _encoder.HashData([
                ("bytes32", _encoder.HashString(Constants.GMX.ACCOUNT_ORDER_LIST_KEY)),
                ("address", publicAddress),
            ])
        }.GetCallData();
        var countCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = countCallData
        };
        var keysCallData = new GetBytes32ValuesAtFunction
        {
            SetKey = _encoder.HashData([
                ("bytes32", _encoder.HashString(Constants.GMX.ACCOUNT_ORDER_LIST_KEY)),
                ("address", publicAddress),
            ]),
            Start = 0,
            End = _defaultCount
        }.GetCallData();
        var keysCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = keysCallData
        };
        aggregateFunction.Calls.Add(countCall);
        aggregateFunction.Calls.Add(keysCall);

        var orderCallData = new GetAccountOrdersFunction
        {
            DataStore = Arbitrum.AddressV2.DataStore,
            Account = publicAddress,
            Start = 0,
            End = _defaultCount
        }.GetCallData();
        var orderCall = new Call
        {
            Target = Arbitrum.AddressV2.Reader,
            CallData = orderCallData
        };
        aggregateFunction.Calls.Add(orderCall);

        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var returnCalls = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        var count = new GetBytes32CountOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex()).ReturnValue1;
        var keys = new GetBytes32ValuesAtOutputDTO().DecodeOutput(returnCalls.ReturnData[1].ToHex()).ReturnValue1;
        var orders = new GetAccountOrdersOutputDTO().DecodeOutput(returnCalls.ReturnData[2].ToHex()).ReturnValue1;

        var result = new List<GmxV2Order>();
        for (int i = 0; i < count; i++)
        {
            var key = keys[i];
            var order = orders[i];
            var date = DateHelpers.GetFromUnixTimestamp((int)order.Numbers.UpdatedAtBlock);
            var orderData = new GmxV2Order()
            {
                Key = key,
                Account = order.Addresses.Account,
                Receiver = order.Addresses.Receiver,
                CallbackContract = order.Addresses.CallbackContract,
                MarketAddress = order.Addresses.Market,
                InitialCollateralTokenAddress = order.Addresses.InitialCollateralToken,
                SwapPath = order.Addresses.SwapPath,
                SizeDeltaUsd = order.Numbers.SizeDeltaUsd,
                InitialCollateralDeltaAmount = order.Numbers.InitialCollateralDeltaAmount,
                ContractTriggerPrice = order.Numbers.TriggerPrice,
                ContractAcceptablePrice = order.Numbers.AcceptablePrice,
                ExecutionFee = order.Numbers.ExecutionFee,
                CallbackGasLimit = order.Numbers.CallbackGasLimit,
                MinOutputAmount = order.Numbers.MinOutputAmount,
                UpdatedAtBlock = order.Numbers.UpdatedAtBlock,
                IsLong = order.Flags.IsLong,
                ShouldUnwrapNativeToken = order.Flags.ShouldUnwrapNativeToken,
                IsFrozen = order.Flags.IsFrozen,
                OrderType = MiscExtensions.ParseEnum<GmxV2OrderType>(order.Numbers.OrderType.ToString()),
                DecreasePositionSwapType =
                    MiscExtensions.ParseEnum<GmxV2DecreasePositionSwapType>(order.Numbers.DecreasePositionSwapType
                        .ToString()),
                Data = order,
                Date = date
            };
            result.Add(orderData);
        }

        return result;
    }

    public async Task<bool> CancelOrders(Web3 web3, string accountKey, Enums.Ticker ticker)
    {
        var orders = await new GmxV2Service().GetOrders(web3, accountKey, ticker);

        if (orders.Count == 0)
        {
            return true;
        }

        // Somehow, it's only possible to cancel one order at a time
        var results = new List<bool>();
        foreach (var order in orders)
        {
            var multicallFunction = new MulticallFunction
            {
                Data = []
            };
            var cancelOrderCallData = new CancelOrderFunction
            {
                Key = order.Key
            };
            multicallFunction.Data.Add(cancelOrderCallData.GetCallData());
            var queryHandler = web3.Eth.GetContractTransactionHandler<MulticallFunction>();
            try
            {
                var returnCalls =
                    await queryHandler.SendRequestAndWaitForReceiptAsync(Arbitrum.AddressV2.ExchangeRouter,
                        multicallFunction);
                results.Add(returnCalls.Status.Value == 1);
            }
            catch (Exception e)
            {
                results.Add(false);
            }
        }

        return results.All(r => r) && results.Count == orders.Count;
    }

    public async Task<Trade> IncreasePosition(Web3 web3, string accountKey, Enums.Ticker ticker,
        Enums.TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
    {
        var defaultLeverage = leverage ?? 1;
        var marketAddress = GmxV2Helpers.GetMarketAddress(ticker);
        var fromToken = TokenV2Service.GetByTicker(ticker);
        var multiCallFunction = new MulticallFunction();
        multiCallFunction.Data = new List<byte[]>();
        var executionFee = await GetExecutionFee(web3, GmxV2Enums.TradeType.Increase);
        var usdcToken = TokenV2Service.TOKENS.First(t => t.Symbol == Enums.Ticker.USDC.ToString());
        var collateral = price * quantity;
        var initialCollateralAmount = GmxV2Helpers.ParseValue(collateral, usdcToken.Decimals);

        var triggerPrice = GmxV2Helpers.ConvertToContractPrice(price, fromToken.Decimals, true);
        var sizeDeltaUsd = GmxV2Helpers.ParseValue(collateral * defaultLeverage, 30);
        var acceptablePrice = GmxV2Helpers.GetAcceptablePrice(price, direction == Enums.TradeDirection.Long);
        var totalWntAmount = executionFee.FeeAmount;
        var sendWntFunction = new SendWntFunction
        {
            Receiver = Arbitrum.AddressV2.OrderVault,
            Amount = totalWntAmount
        };
        multiCallFunction.Data.Add(sendWntFunction.GetCallData());

        var sendTokenCallData = new SendTokensFunction
        {
            Token = usdcToken.Address,
            Receiver = Arbitrum.AddressV2.OrderVault,
            Amount = initialCollateralAmount
        };
        multiCallFunction.Data.Add(sendTokenCallData.GetCallData());

        var createOrderCallData = new CreateOrderFunction
        {
            Params = new CreateOrderParams
            {
                Addresses = new CreateOrderParamsAddresses
                {
                    CallbackContract = Arbitrum.Address.Zero,
                    CancellationReceiver = Arbitrum.Address.Zero,
                    InitialCollateralToken = usdcToken.Address,
                    Market = marketAddress,
                    Receiver = accountKey,
                    SwapPath = new List<string>(),
                    UiFeeReceiver = Arbitrum.Address.Zero
                },
                AutoCancel = false,
                IsLong = direction == Enums.TradeDirection.Long,
                DecreasePositionSwapType = 0,
                Numbers = new CreateOrderParamsNumbers
                {
                    AcceptablePrice = acceptablePrice,
                    CallbackGasLimit = 0,
                    ExecutionFee = executionFee.FeeAmount,
                    InitialCollateralDeltaAmount = initialCollateralAmount, // Should be 0 somehow
                    SizeDeltaUsd = sizeDeltaUsd,
                    TriggerPrice = triggerPrice,
                    MinOutputAmount = 0,
                },
                OrderType = 3,
                ReferralCode = new byte[32],
                ShouldUnwrapNativeToken = false
            }
        };
        multiCallFunction.Data.Add(createOrderCallData.GetCallData());
        multiCallFunction.Gas = new HexBigInteger(8_000_000);
        var gasPriceData = await GetCurrentGasPrice(web3);
        multiCallFunction.GasPrice = new HexBigInteger(gasPriceData);
        multiCallFunction.AmountToSend = totalWntAmount;

        try
        {
            var exchangeRouterService = new ExchangeRouterService(web3, Arbitrum.AddressV2.ExchangeRouter);
            var receipt = await exchangeRouterService.MulticallRequestAndWaitForReceiptAsync(multiCallFunction);

            // Call privy api instead and send the txn
            var trade = new Trade(DateTime.UtcNow,
                direction,
                Enums.TradeStatus.Requested,
                Enums.TradeType.Limit,
                ticker,
                quantity,
                price,
                leverage,
                receipt.TransactionHash,
                "");

            return trade;
        }
        catch (Exception e)
        {
            Console.WriteLine($"Error increasing position: {e.Message}");
            throw;
        }
    }

    public async Task<Trade> DecreasePosition(
        Web3 web3,
        string accountAddress,
        Enums.Ticker ticker,
        Enums.TradeDirection direction,
        decimal closePrice,
        decimal quantity,
        decimal leverage)
    {
        var trade = new Trade(DateTime.UtcNow,
            direction,
            Enums.TradeStatus.Cancelled,
            Enums.TradeType.StopMarket,
            ticker,
            quantity,
            closePrice,
            leverage,
            "",
            "");

        var marketAddress = GmxV2Helpers.GetMarketAddress(ticker);
        var currentPosition =
            (await GetGmxPositionsV2(web3, accountAddress)).FirstOrDefault(p =>
                GmxV2Helpers.SameAddress(marketAddress, p.MarketAddress));

        if (currentPosition == null || currentPosition.SizeInUsd == 0) return trade;


        // var collateralToken = TokenV2Service.TOKENS.First(t => t.Symbol == "USDC");
        var executionFee = await GetExecutionFee(web3, GmxV2Enums.TradeType.Decrease);
        var acceptablePrice = GmxV2Helpers.GetAcceptablePrice(closePrice, direction == Enums.TradeDirection.Long);
        // var collateral = closePrice * quantity;
        // var initialCollateralAmount = GmxV2Helpers.ParseValue(collateral, collateralToken.Decimals);
        var quantityLeveraged = quantity * leverage;
        // var sizeDeltaUsd = GmxV2Helpers.ParseValue(quantityLeveraged * closePrice, 0);
        var sendWntFunction = new SendWntFunction
        {
            Receiver = Arbitrum.AddressV2.OrderVault,
            Amount = executionFee.FeeAmount
        }.GetCallData();

        var createOrderFunction = new CreateOrderFunction
        {
            Params = new CreateOrderParams
            {
                Addresses = new CreateOrderParamsAddresses
                {
                    Market = marketAddress,
                    InitialCollateralToken = currentPosition.CollateralTokenAddress,
                    CancellationReceiver = Arbitrum.Address.Zero,
                    Receiver = accountAddress,
                    CallbackContract = Arbitrum.Address.Zero,
                    SwapPath = new List<string>(),
                    UiFeeReceiver = Arbitrum.Address.Zero
                },
                Numbers = new CreateOrderParamsNumbers
                {
                    SizeDeltaUsd = currentPosition.SizeInUsd,
                    InitialCollateralDeltaAmount = currentPosition.CollateralAmount, // No collateral withdrawal
                    TriggerPrice = 0,
                    AcceptablePrice = acceptablePrice,
                    ExecutionFee = executionFee.FeeAmount,
                    MinOutputAmount = 0,
                    ValidFromTime = 0,
                    CallbackGasLimit = 0
                },
                OrderType = 4,
                DecreasePositionSwapType = 1,
                IsLong = currentPosition.IsLong,
                ShouldUnwrapNativeToken = false,
                ReferralCode = new byte[32]
            }
        };

        // 5. Configure transaction
        var gasPriceData = await GetCurrentGasPrice(web3);

        var multicallFunction = new MulticallFunction
        {
            Data = new List<byte[]> { sendWntFunction, createOrderFunction.GetCallData() },
            AmountToSend = executionFee.FeeAmount,
            Gas = new HexBigInteger(7_000_000),
            GasPrice = new HexBigInteger(gasPriceData)
        };

        try
        {
            // 7. Execute transaction
            var exchangeRouterService = new ExchangeRouterService(web3, Arbitrum.AddressV2.ExchangeRouter);
            var receipt = await exchangeRouterService.MulticallRequestAndWaitForReceiptAsync(
                multicallFunction
            );

            return new Trade(
                DateTime.UtcNow,
                direction,
                Enums.TradeStatus.Requested,
                Enums.TradeType.Market,
                ticker,
                quantity,
                closePrice,
                leverage,
                receipt.TransactionHash,
                ""
            );
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Decrease position failed: {ex.Message}");
            trade.SetMessage(ex.Message);
            return trade;
        }
    }

// V2-specific helper methods
    private (BigInteger triggerPrice, BigInteger acceptablePrice) GetDecreaseOrderPrices(
        decimal price,
        bool isLong,
        int priceDecimals,
        decimal slippageBps)
    {
        var slippageMultiplier = isLong
            ? 1m - (slippageBps / 10000m)
            : 1m + (slippageBps / 10000m);

        return (
            GmxV2Helpers.ConvertToContractPrice(price, priceDecimals),
            GmxV2Helpers.ConvertToContractPrice(price * slippageMultiplier, 30)
        );
    }

    private async Task<HexBigInteger> GetCurrentGasPrice(Web3 web3)
    {
        var gasPrice = await web3.Eth.GasPrice.SendRequestAsync();
        return new HexBigInteger(gasPrice.Value * 115 / 100); // Add 15% buffer
    }

    public async Task<GmxV2GasLimit> GetGasLimit(Web3 web3)
    {
        var aggregateFunction = new AggregateFunction();
        aggregateFunction.Calls = new List<Call>();

        var depositSingleTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.DEPOSIT_GAS_LIMIT_KEY)),
                    ("bool", true)
                ])
            }.GetCallData()
        };
        var depositMultiTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.DEPOSIT_GAS_LIMIT_KEY)),
                    ("bool", false)
                ])
            }.GetCallData()
        };
        var withdrawalMultiTokenCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashData([
                    ("bytes32", _encoder.HashString(Constants.GMX.WITHDRAWAL_GAS_LIMIT_KEY)),
                ])
            }.GetCallData()
        };
        var shiftCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.SHIFT_GAS_LIMIT_KEY)
            }.GetCallData()
        };
        var singleSwapCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.SINGLE_SWAP_GAS_LIMIT_KEY)
            }.GetCallData()
        };
        var swapOrderCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.SWAP_ORDER_GAS_LIMIT_KEY)
            }.GetCallData()
        };
        var increaseOrderCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.INCREASE_ORDER_GAS_LIMIT_KEY)
            }.GetCallData()
        };
        var decreaseOrderCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.DECREASE_ORDER_GAS_LIMIT_KEY)
            }.GetCallData()
        };
        var estimatedGasFeeBaseAmountCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.ESTIMATED_GAS_FEE_BASE_AMOUNT)
            }.GetCallData()
        };
        var estimatedGasFeePerOraclePriceCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.ESTIMATED_GAS_FEE_PER_ORACLE_PRICE)
            }.GetCallData()
        };
        var estimatedFeeMultiplierFactorCall = new Call
        {
            Target = Arbitrum.AddressV2.DataStore,
            CallData = new GetUintFunction
            {
                Key = _encoder.HashString(Constants.GMX.ESTIMATED_GAS_FEE_MULTIPLIER_FACTOR)
            }.GetCallData()
        };

        aggregateFunction.Calls.Add(depositSingleTokenCall);
        aggregateFunction.Calls.Add(depositMultiTokenCall);
        aggregateFunction.Calls.Add(withdrawalMultiTokenCall);
        aggregateFunction.Calls.Add(shiftCall);
        aggregateFunction.Calls.Add(singleSwapCall);
        aggregateFunction.Calls.Add(swapOrderCall);
        aggregateFunction.Calls.Add(increaseOrderCall);
        aggregateFunction.Calls.Add(decreaseOrderCall);
        aggregateFunction.Calls.Add(estimatedGasFeeBaseAmountCall);
        aggregateFunction.Calls.Add(estimatedGasFeePerOraclePriceCall);
        aggregateFunction.Calls.Add(estimatedFeeMultiplierFactorCall);

        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var returnCalls = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        var gasLimit = new GmxV2GasLimit();
        gasLimit.DepositSingleToken =
            new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex()).ReturnValue1;
        gasLimit.DepositMultiToken =
            new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[1].ToHex()).ReturnValue1;
        gasLimit.WithdrawalMultiToken =
            new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[2].ToHex()).ReturnValue1;
        gasLimit.Shift = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[3].ToHex()).ReturnValue1;
        gasLimit.SingleSwap = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[4].ToHex()).ReturnValue1;
        gasLimit.SwapOrder = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[5].ToHex()).ReturnValue1;
        gasLimit.IncreaseOrder = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[6].ToHex()).ReturnValue1;
        gasLimit.DecreaseOrder = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[7].ToHex()).ReturnValue1;
        gasLimit.EstimatedGasFeeBaseAmount =
            new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[8].ToHex()).ReturnValue1;
        gasLimit.EstimatedGasFeePerOraclePrice =
            new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[9].ToHex()).ReturnValue1;
        gasLimit.EstimatedFeeMultiplierFactor =
            new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[10].ToHex()).ReturnValue1;

        return gasLimit;
    }

    public async Task<GmxV2ExecutionFee> GetExecutionFee(
        Web3 web3,
        GmxV2Enums.TradeType tradeType,
        int swapPathLength = 0)
    {
        const int PRECISION = 30;

        // 1. Get gas configuration from chain
        var gasLimits = await GetGasLimits(web3);
        var gasPrice = await web3.Eth.GasPrice.SendRequestAsync();
        var tokensData = await GetTokensData(web3);

        // 2. Calculate oracle price count (matches SDK logic)
        var oraclePriceCount = (swapPathLength > 0 ? 2 : 1) * (swapPathLength + 1);

        // 3. Get base gas limit with oracle cost
        var operationGasLimit = tradeType switch
        {
            GmxV2Enums.TradeType.Increase => 6_500_000,
            GmxV2Enums.TradeType.Decrease => 5_500_000,
            _ => throw new ArgumentException("Invalid trade type")
        };

        var feeAmount = operationGasLimit * gasPrice.Value;

        // 5. Apply multiplier with precise integer math
        var applyFactor = (operationGasLimit * gasLimits.EstimatedFeeMultiplierFactor) /
                          BigInteger.Pow(10, PRECISION);

        // 7. Get native token data
        var nativeToken = tokensData.First(t =>
            t.Symbol.Equals("ETH", StringComparison.OrdinalIgnoreCase) ||
            t.Symbol.Equals("WETH", StringComparison.OrdinalIgnoreCase));

        return new GmxV2ExecutionFee
        {
            FeeAmount = feeAmount,
            // FeeUsd = ConvertToUsd(feeAmount, nativeToken.Decimals, nativeToken.Price.Min),
            NativeToken = nativeToken
        };
    }

// Helper methods
    private BigInteger ConvertToUsd(BigInteger feeAmount, int decimals, decimal tokenPrice)
    {
        var tokenValue = (decimal)feeAmount / (decimal)BigInteger.Pow(10, decimals);
        return (BigInteger)(tokenValue * tokenPrice);
    }

    private async Task<GmxV2GasLimit> GetGasLimits(Web3 web3)
    {
        // Implement chain-specific gas limit configuration
        return new GmxV2GasLimit
        {
            EstimatedGasFeeBaseAmount = 200_000,
            EstimatedGasFeePerOraclePrice = 50_000,
            EstimatedFeeMultiplierFactor = BigInteger.Parse("1000000000000000000") // 1e18
        };
    }

// Helper methods
    private BigInteger EstimateOrderOraclePriceCount(int swapPathLength)
    {
        // Matches SDK's estimateOrderOraclePriceCount
        return (swapPathLength > 0 ? 2 : 1) * (swapPathLength + 1);
    }

    private async Task<BigInteger> GetGasPriceInWei(Web3 web3)
    {
        var gasPrice = await web3.Eth.GasPrice.SendRequestAsync();
        return gasPrice.Value; // Already in wei
    }

    private BigInteger GetOperationGasLimit(GmxV2Enums.TradeType tradeType)
    {
        // Implement based on GMX's internal logic:
        // - Increase: 6_500_000
        // - Decrease: 7_000_000  
        // - Swap: 8_000_000
        return tradeType switch
        {
            GmxV2Enums.TradeType.Increase => 6_500_000,
            GmxV2Enums.TradeType.Decrease => 7_000_000,
            GmxV2Enums.TradeType.Swap => 8_000_000,
            _ => throw new ArgumentException("Invalid trade type")
        };
    }

    public BigInteger GetEstimatedGasLimit(GmxV2GasLimit gasLimit, int swapCount,
        GmxV2Enums.TradeType tradeType)
    {
        switch (tradeType)
        {
            case GmxV2Enums.TradeType.Swap:
                break;
            case GmxV2Enums.TradeType.Increase:
                // To handle collateral swap must implement get swap path
                return EstimateExecuteIncreaseOrderGasLimit(gasLimit, swapCount);
            case GmxV2Enums.TradeType.Decrease:
                return EstimateExecuteDecreaseOrderGasLimit(gasLimit, swapCount);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(tradeType), tradeType, null);
        }

        return new BigInteger();
    }

    private BigInteger EstimateExecuteIncreaseOrderGasLimit(GmxV2GasLimit gasLimit, int? swapCount = null,
        BigInteger? callbackGasLimit = null)
    {
        var gasPerSwap = gasLimit.SingleSwap;
        var swapsCount = new BigInteger(swapCount ?? 0);
        var totalGasLimit = gasLimit.IncreaseOrder + gasPerSwap * swapsCount +
                            (callbackGasLimit ?? BigInteger.Zero);
        return totalGasLimit;
    }

    private BigInteger EstimateExecuteDecreaseOrderGasLimit(GmxV2GasLimit gasLimit, int swapCount,
        BigInteger? callbackGasLimit = null, GmxV2Enums.DecreasePositionSwapType? decreaseSwapType = null)
    {
        var gasPerSwap = gasLimit.SingleSwap;
        var swapsCount = new BigInteger(swapCount);

        if (decreaseSwapType != GmxV2Enums.DecreasePositionSwapType.NoSwap)
        {
            swapsCount += 1;
        }

        var totalGasLimit = gasLimit.DecreaseOrder + gasPerSwap * swapsCount + (callbackGasLimit ?? BigInteger.Zero);
        return totalGasLimit;
    }

    public async Task<Trade> GetTrade(Web3 web3, string publicAddress, Enums.Ticker ticker)
    {
        var position = await GetGmxPositionsV2(web3, publicAddress);
        var positionsPerTicker =
            position.First(p => GmxV2Helpers.SameAddress(p.Key, publicAddress));

        return GmxV2Mappers.Map(positionsPerTicker, ticker);
    }

    private async Task<List<GmxV2Position>> GetGmxPositionsV2(Web3 web3, string account)
    {
        var marketsInfoData = await GetMarketInfosAsync(web3);
        var tokensData = await GetTokensData(web3, true);
        var (marketKeys, marketPrices, allPositionsKeys) = GetKeysAndPrices(marketsInfoData, tokensData, account);

        var getAccountPositionInfoListFunction = new GetAccountPositionInfoListFunction
        {
            DataStore = Arbitrum.AddressV2.DataStore,
            ReferralStorage = Arbitrum.AddressV2.ReferralStorage,
            Account = account,
            Markets = marketKeys,
            MarketPrices = GmxV2Mappers.Map(marketPrices),
            UiFeeReceiver = Arbitrum.Address.Zero,
            Start = new BigInteger(0),
            End = new BigInteger(1000)
        }.GetCallData();

        var getPositionCall = new Call()
        {
            Target = Arbitrum.AddressV2.SyntheticsReader,
            CallData = getAccountPositionInfoListFunction
        };

        var aggregateFunction = new AggregateFunction();
        aggregateFunction.Calls = new List<Call>();
        aggregateFunction.Calls.Add(getPositionCall);

        var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
        var returnCalls = await queryHandler
            .QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
            .ConfigureAwait(false);

        var hexResult = returnCalls.ReturnData[0].ToHex();

        var positions = new GetAccountPositionInfoListOutputDTO().DecodeOutput(hexResult).ReturnValue1;
        var result = new List<GmxV2Position>();
        foreach (var positionInfo in positions)
        {
            var position = positionInfo.Position;
            var fees = positionInfo.Fees;
            var addresses = position.Addresses;
            var numbers = position.Numbers;
            var flags = position.Flags;
            var marketInfo =
                marketsInfoData.First(m => GmxV2Helpers.SameAddress(m.Market.MarketToken, addresses.Market));
            var tokenData = TokenV2Service.GetByTicker(Enums.Ticker.USDC);

            if (numbers.IncreasedAtTime == 0)
            {
                continue;
            }

            var positionKey = GmxV2Helpers.GetPositionKey(addresses.Account, addresses.Market,
                addresses.CollateralToken, flags.IsLong);

            var contractPositionKey = _encoder.HashData([
                ("address", addresses.Account),
                ("address", addresses.Market),
                ("address", addresses.CollateralToken),
                ("bool", flags.IsLong)
            ]);

            var gmxPosition = new GmxV2Position
            {
                Key = positionKey,
                ContractKey = contractPositionKey.ToString(),
                Account = addresses.Account,
                MarketAddress = addresses.Market,
                CollateralTokenAddress = addresses.CollateralToken,
                SizeInUsd = numbers.SizeInUsd,
                SizeInTokens = numbers.SizeInTokens,
                CollateralAmount = numbers.CollateralAmount,
                IncreasedAtTime = numbers.IncreasedAtTime,
                DecreasedAtTime = numbers.DecreasedAtTime,
                IsLong = flags.IsLong,
                PendingBorrowingFeesUsd = fees.Borrowing.BorrowingFeeUsd,
                FundingFeeAmount = fees.Funding.FundingFeeAmount,
                ClaimableLongTokenAmount = fees.Funding.ClaimableLongTokenAmount,
                ClaimableShortTokenAmount = fees.Funding.ClaimableShortTokenAmount,
                MarketInfo = marketInfo,
                TokenData = tokenData
            };

            result.Add(gmxPosition);
        }

        return result;
    }

    private (List<string> marketKeys, List<MarketPrices> marketPrices, List<string> allPositionsKeys) GetKeysAndPrices(
        List<GmxMarketInfo> marketsInfoData, List<GmxTokenData> tokensData, string account)
    {
        var values = (
            marketKeys: new List<string>(),
            marketPrices: new List<MarketPrices>(),
            allPositionsKeys: new List<string>()
        );

        if (string.IsNullOrEmpty(account) || marketsInfoData == null || tokensData == null)
        {
            return values;
        }

        foreach (var market in marketsInfoData)
        {
            var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market.Market);

            if (marketPrices == null || market.Market.IsSpotOnly)
            {
                continue;
            }

            values.marketKeys.Add(market.Market.MarketToken);
            values.marketPrices.Add(marketPrices);

            var collaterals = market.Market.IsSameCollaterals
                ? new List<string> { market.Market.LongToken }
                : new List<string> { market.Market.LongToken, market.Market.ShortToken };

            foreach (var collateralAddress in collaterals)
            {
                foreach (var isLong in new[] { true, false })
                {
                    var positionKey = GetPositionKeyV2(account, market.Market.MarketToken, collateralAddress, isLong);
                    values.allPositionsKeys.Add(positionKey);
                }
            }
        }

        return values;
    }


    // Helper Methods
    private static string GetPositionKeyV2(string account, string market, string collateralToken, bool isLong)
    {
        return $"{account}-{market}-{collateralToken}-{(isLong ? "true" : "false")}";
    }

    public async Task<decimal> QuantityInPosition(Web3 web3, string publicAddress, Enums.Ticker ticker)
    {
        var trade = await GetTrade(web3, publicAddress, ticker);
        return trade.Status == Enums.TradeStatus.Filled ? trade.Quantity : 0;
    }
}