using System.Numerics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Models.Gmx.v1;
using Managing.Infrastructure.Evm.Referentials;
using Managing.Tools.ABI.OrderBook;
using Managing.Tools.ABI.OrderBook.ContractDefinition;
using Managing.Tools.ABI.OrderBookReader;
using Managing.Tools.ABI.OrderBookReader.ContractDefinition;
using Managing.Tools.ABI.PositionRouter;
using Managing.Tools.ABI.PositionRouter.ContractDefinition;
using Managing.Tools.ABI.Reader;
using Managing.Tools.ABI.Reader.ContractDefinition;
using Managing.Tools.ABI.Router;
using Managing.Tools.ABI.Router.ContractDefinition;
using Nethereum.Contracts.Standards.ERC20;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Nethereum.Util;
using Nethereum.Web3;
using static Managing.Common.Enums;
using ApproveFunction = Nethereum.Contracts.Standards.ERC20.ContractDefinition.ApproveFunction;

namespace Managing.Infrastructure.Evm.Services.Gmx;

public static class GmxService
{
    private const decimal _orderFeesExecution = 0.0003m;
    private const decimal _positionUpdateFees = 0.0001m;

    public static async Task InitAccountForTrading(Web3 web3, string publicAddress, List<Ticker> tickers)
    {
        var router = new RouterService(web3, Arbitrum.Address.Router);
        if (!await IsPluginAdded(web3, publicAddress, Arbitrum.Address.PositionRouter))
        {
            var routerApproval = await router
                .ApprovePluginRequestAndWaitForReceiptAsync(Arbitrum.Address.PositionRouter);
        }


        if (!await IsPluginAdded(web3, publicAddress, Arbitrum.Address.OrderBook))
        {
            var routerApproval = await router
                .ApprovePluginRequestAsync(Arbitrum.Address.OrderBook);
        }

        foreach (var ticker in tickers)
        {
            var conntractAddress = TokenService.GetContractAddress(ticker);
            await EvmBase.ApproveToken(web3, publicAddress, conntractAddress, publicAddress);
        }
    }

    public static async Task<bool> IsPluginAdded(Web3 web3, string publicAddress, string pluginAddress)
    {
        var router = new RouterService(web3, Arbitrum.Address.Router);
        var function = new ApprovedPluginsFunction
        {
            ReturnValue1 = publicAddress,
            ReturnValue2 = pluginAddress
        };

        var isAdded = await router.ApprovedPluginsQueryAsync(function);
        return isAdded;
    }


    public static async Task<bool> ApproveOrder(Web3 web3, Ticker ticker, string publicAddress, decimal amount)
    {
        var contractAddress = TokenService.GetContractAddress(ticker);
        var contract = new ERC20ContractService(web3.Eth, contractAddress);

        var allowanceQuery = new AllowanceFunction
        {
            Owner = publicAddress,
            Spender = Arbitrum.Address.Router
        };

        var allowance = await contract.AllowanceQueryAsync(allowanceQuery);

        if (allowance.IsZero) return false;

        var approveQuery = new ApproveFunction
        {
            FromAddress = publicAddress,
            Spender = Arbitrum.Address.Router,
            Value = Web3.Convert.ToWei(amount)
        };

        var approval = await contract.ApproveRequestAsync(approveQuery);
        return true;
    }

    public static async Task<Trade> IncreasePosition(Web3 web3, string publicAddress, Ticker ticker,
        TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
    {
        var quantityLeveraged = GmxHelpers.GetQuantityForLeverage(quantity, leverage);
        var orderBook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
        var contractAddress = TokenService.GetContractAddress(ticker);
        var isLong = direction == TradeDirection.Long;
        var function = new CreateIncreaseOrderFunction();

        // Forcing path to use USDC to pay the trade
        function.Path = new List<string> { Arbitrum.Address.USDC };
        function.AmountIn = Web3.Convert.ToWei(quantity * price, 6); // Price in $ to pay the long/short. Ex 11.42$
        function.IndexToken = contractAddress; // Token to long/short
        function.MinOut = new BigInteger(0);
        // Size of the position with the leveraged quantity
        // Ex : Long 11$ x3. SizeDelta = 33$
        function.SizeDelta = Web3.Convert.ToWei(quantityLeveraged * price, UnitConversion.EthUnit.Tether);
        function.CollateralToken = Arbitrum.Address.USDC; // USDC 
        function.IsLong = isLong;
        function.TriggerPrice = Web3.Convert.ToWei(price, 30); // Price of the order execution
        function.TriggerAboveThreshold = false;
        function.ExecutionFee = Web3.Convert.ToWei(_orderFeesExecution); // Fee required to execute tx
        function.ShouldWrap = false;

        // Specify the tx opts
        function.AmountToSend = Web3.Convert.ToWei(_orderFeesExecution);
        function.FromAddress = publicAddress;
        //function.MaxFeePerGas = await orderBook.ContractHandler.EstimateGasAsync(function);
        function.GasPrice = GetGasPrice();

        // Approving Router to transfer ERC20 token
        var approval = await ApproveOrder(web3, Ticker.USDC, publicAddress, _orderFeesExecution);

        if (!approval) return null;

        var receipt = await orderBook
            .CreateIncreaseOrderRequestAndWaitForReceiptAsync(function);

        var trade = new Trade(DateTime.UtcNow,
            direction,
            TradeStatus.Requested,
            TradeType.Limit,
            ticker,
            quantity,
            price,
            leverage,
            receipt.TransactionHash,
            "");

        return trade;
    }

    public static async Task<Trade> DecreasePosition(Web3 web3, string publicAddress, Ticker ticker,
        TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
    {
        var trade = new Trade(DateTime.UtcNow,
            direction,
            TradeStatus.Cancelled,
            TradeType.Market,
            ticker,
            quantity,
            price,
            leverage,
            "",
            "");

        // Check if there is quantity in position
        if (await QuantityInPosition(web3, publicAddress, ticker) == 0) return trade;

        var quantityLeveraged = GmxHelpers.GetQuantityForLeverage(quantity, leverage);
        var positionRouter = new PositionRouterService(web3, Arbitrum.Address.PositionRouter);
        var contractAddress = TokenService.GetContractAddress(ticker);
        var isLong = direction == TradeDirection.Long;
        var function = new CreateDecreasePositionFunction();

        // Forcing path to use contract address to widthdraw funds
        // The address for closing a short, should be USDC
        //function.Path = new List<string> { contractAddress };
        function.Path = new List<string> { Arbitrum.Address.USDC };
        // the index token of the position
        function.IndexToken = contractAddress; // Token to long/short
        // the amount of collateral in USD value to withdraw
        function.CollateralDelta = new BigInteger(0); // Price in $ to pay the long/short. Ex 11.42$
        //function.CollateralDelta = Web3.Convert.ToWei(quantity * price, 6); // Price in $ to pay the long/short. Ex 11.42$
        // the USD value of the change in position size
        function.SizeDelta = Web3.Convert.ToWei(quantity, UnitConversion.EthUnit.Tether);
        function.IsLong = isLong;
        // the address to receive the withdrawn tokens
        function.Receiver = publicAddress;
        // the USD value of the min (for longs) or max (for shorts) index price acceptable when executing the request
        function.AcceptablePrice = GmxHelpers.GetAcceptablePrice(price, isLong);
        // the min output token amount
        function.MinOut = new BigInteger(0);
        function.ExecutionFee = Web3.Convert.ToWei(_positionUpdateFees); // Fee required to execute tx
        function.WithdrawETH = false;
        function.CallbackTarget = Arbitrum.Address.Zero;

        // Specify the tx opts
        function.AmountToSend = Web3.Convert.ToWei(_positionUpdateFees);
        function.FromAddress = publicAddress;
        function.MaxFeePerGas = await positionRouter.ContractHandler.EstimateGasAsync(function);
        function.GasPrice = GetGasPrice();

        var approval = await ApproveOrder(web3, ticker, publicAddress, _positionUpdateFees);

        if (!approval) return null;

        var receipt = await positionRouter
            .CreateDecreasePositionRequestAndWaitForReceiptAsync(function);

        trade.SetExchangeOrderId(receipt.TransactionHash);
        trade.SetStatus(receipt.Status.Value.IsOne ? TradeStatus.Requested : TradeStatus.Cancelled);

        return trade;
    }

    public static async Task<Trade> DecreaseOrder(Web3 web3, TradeType tradeType, string publicAddress, Ticker ticker,
        TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
    {
        var trade = new Trade(DateTime.UtcNow,
            direction,
            TradeStatus.Cancelled,
            tradeType,
            ticker,
            quantity,
            price,
            leverage,
            "",
            "");

        // Check if there is quantity in position
        var currentPosition = await GetGmxPosition(web3, publicAddress, ticker);

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

        var quantityLeveraged = GmxHelpers.GetQuantityForLeverage(quantity, leverage);
        var orderbook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
        var contractAddress = TokenService.GetContractAddress(ticker);
        var isLong = direction != TradeDirection.Long;
        var function = new CreateDecreaseOrderFunction();

        // the index token of the position
        function.IndexToken = contractAddress; // Token to long/short
        // the USD value of the change in position size
        function.SizeDelta = currentPosition.SizeDelta;
        function.CollateralToken = Arbitrum.Address.USDC;
        // the amount of collateral in USD value to withdraw
        function.CollateralDelta = new BigInteger(0); // Price in $ to pay the long/short. Ex 11.42$
        //function.CollateralDelta = Web3.Convert.ToWei(quantity * price, 6); // Price in $ to pay the long/short. Ex 11.42$
        function.IsLong = isLong;
        // the USD value of the min (for longs) or max (for shorts) index price acceptable when executing the request
        function.TriggerPrice = GmxHelpers.GetAcceptablePrice(price, isLong);
        function.TriggerAboveThreshold = GmxHelpers.GetTriggerAboveThreshold(isLong, tradeType);

        // Specify the tx opts
        function.AmountToSend = Web3.Convert.ToWei(_orderFeesExecution);
        function.FromAddress = publicAddress;
        function.MaxFeePerGas = await orderbook.ContractHandler.EstimateGasAsync(function);
        function.GasPrice = GetGasPrice();

        var approval = await ApproveOrder(web3, ticker, publicAddress, _positionUpdateFees);

        if (!approval) return null;

        var receipt = await orderbook
            .CreateDecreaseOrderRequestAndWaitForReceiptAsync(function);

        trade.SetExchangeOrderId(receipt.TransactionHash);
        trade.SetStatus(TradeStatus.Requested);
        return trade;
    }

    public static async Task<bool> CancelOrders(Web3 web3, string publicAddress, Ticker ticker)
    {
        var orderBook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
        var orders = await GetOrders(web3, publicAddress, ticker);

        if (!orders.Any()) return true;

        var function = new CancelMultipleFunction();
        var increaseOrderIndexes = orders.Where(i => i.Type == GmxOrderType.Increase);
        function.IncreaseOrderIndexes = increaseOrderIndexes.Select(o => o.Index).ToList();

        var decreaseOrderIndexes = orders.Where(i => i.Type == GmxOrderType.Decrease);
        function.DecreaseOrderIndexes = decreaseOrderIndexes.Select(o => o.Index).ToList();

        function.SwapOrderIndexes = new List<BigInteger>();

        try
        {
            if (function.DecreaseOrderIndexes.Any() || function.IncreaseOrderIndexes.Any())
            {
                function.MaxFeePerGas = await orderBook.ContractHandler.EstimateGasAsync(function);
                function.GasPrice = GetGasPrice();

                var cancellation = await orderBook.CancelMultipleRequestAndWaitForReceiptAsync(function);
            }
        }
        catch (Exception ex)
        {
            return false;
        }

        return true;
    }

    private static BigInteger GetGasPrice()
    {
        return Web3.Convert.ToWei(0.1, UnitConversion.EthUnit.Gwei);
    }

    public static async Task<List<GmxOrder>> GetOrders(Web3 web3, string publicAddress, Ticker ticker)
    {
        var lastIndexes = await GetLastIndex(web3, publicAddress);
        var orders = new List<GmxOrder>();
        var orderBookReader = new OrderBookReaderService(web3, Arbitrum.Address.OrderBookReader);

        var increaseOrders = await GetIncreaseOrders(orderBookReader, publicAddress, lastIndexes.IncreaseIndex);
        var decreaseOrders = await GetDecreaseOrders(orderBookReader, publicAddress, lastIndexes.DecreaseIndex);

        orders.AddRange(increaseOrders);
        orders.AddRange(decreaseOrders);
        var contractAddress = TokenService.GetContractAddress(ticker);
        var ordersFiltered = orders.Where(o =>
            string.Equals(o.IndexToken, contractAddress, StringComparison.CurrentCultureIgnoreCase));

        return ordersFiltered.ToList();
    }

    private static async Task<List<GmxOrder>> GetIncreaseOrders(OrderBookReaderService orderBookReader,
        string publicAddress, int lastIndex)
    {
        var increaseIndex = GmxHelpers.GetIndexesRange(lastIndex);
        var increaseOrdersFunction = new GetIncreaseOrdersFunction
        {
            OrderBookAddress = Arbitrum.Address.OrderBook,
            Account = publicAddress,
            Indices = increaseIndex,
        };

        var increaseOrders = await orderBookReader.GetIncreaseOrdersQueryAsync(increaseOrdersFunction);

        return GmxMappers.MapIncrease(increaseOrders.ReturnValue1, increaseOrders.ReturnValue2, increaseIndex);
    }

    private static async Task<List<GmxOrder>> GetDecreaseOrders(OrderBookReaderService orderBookReader,
        string publicAddress, int lastIndex)
    {
        var increaseIndex = GmxHelpers.GetIndexesRange(lastIndex);
        var increaseOrdersFunction = new GetDecreaseOrdersFunction
        {
            OrderBookAddress = Arbitrum.Address.OrderBook,
            Account = publicAddress,
            Indices = increaseIndex,
        };

        var increaseOrders = await orderBookReader.GetDecreaseOrdersQueryAsync(increaseOrdersFunction);

        return GmxMappers.MapDecrease(increaseOrders.ReturnValue1, increaseOrders.ReturnValue2, increaseIndex);
    }

    public static async Task<GmxOrderIndexes> GetLastIndex(Web3 web3, string publicAddress)
    {
        var orderBook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
        var increaseFunction = new IncreaseOrdersIndexFunction
        {
            ReturnValue1 = publicAddress
        };
        var decreaseFunction = new DecreaseOrdersIndexFunction
        {
            ReturnValue1 = publicAddress
        };
        var swapFunction = new SwapOrdersIndexFunction
        {
            ReturnValue1 = publicAddress
        };

        var increaseIndex = await orderBook.IncreaseOrdersIndexQueryAsync(increaseFunction);
        var decreaseIndex = await orderBook.DecreaseOrdersIndexQueryAsync(decreaseFunction);
        var swapIndex = await orderBook.SwapOrdersIndexQueryAsync(swapFunction);

        var indexes = new GmxOrderIndexes
        {
            SwapIndex = (int)swapIndex > 0 ? (int)swapIndex - 1 : (int)swapIndex,
            IncreaseIndex = (int)increaseIndex > 0 ? (int)increaseIndex - 1 : (int)increaseIndex,
            DecreaseIndex = (int)decreaseIndex > 0 ? (int)decreaseIndex - 1 : (int)decreaseIndex
        };

        return indexes;
    }

    public static async Task<Trade> GetTrade(Web3 web3, string publicAddress, Ticker ticker)
    {
        var position = await GetGmxPosition(web3, publicAddress, ticker);
        return GmxMappers.Map(position, ticker);
    }

    public static async Task<GmxPosition> GetGmxPosition(Web3 web3, string publicAddress, Ticker ticker)
    {
        var reader = new ReaderService(web3, Arbitrum.Address.Reader);
        var contractAddress = TokenService.GetContractAddress(ticker);
        var queryData = GmxHelpers.GetPositionQueryData(new List<string> { contractAddress });

        var function = new GetPositionsFunction
        {
            Vault = Arbitrum.Address.Vault,
            Account = publicAddress,
            CollateralTokens = queryData.CollateralTokens,
            IndexTokens = queryData.IndexTokens,
            IsLong = queryData.IsLong
        };

        var result = await reader.GetPositionsQueryAsync(function);

        var positions = GmxMappers.MapPositions(result, queryData);
        var position = positions.FirstOrDefault(p => p.IndexToken == contractAddress);
        return position;
    }

    public static async Task<decimal> QuantityInPosition(Web3 web3, string key, Ticker ticker)
    {
        var position = await GetTrade(web3, key, ticker);
        return position?.Quantity ?? 0m;
    }

    public static async Task<decimal> GetFee(Web3 web3, decimal ethPrice)
    {
        var positionRouter = new PositionRouterService(web3, Arbitrum.Address.PositionRouter);
        var contractAddress = TokenService.GetContractAddress(Ticker.BTC);
        var function = new CreateDecreasePositionFunction();

        function.Path = new List<string> { contractAddress };
        function.IndexToken = contractAddress; // Token to long/short
        function.CollateralDelta = new BigInteger(0); // Price in $ to pay the long/short. Ex 11.42$
        function.SizeDelta = Web3.Convert.ToWei(100, UnitConversion.EthUnit.Tether);
        function.IsLong = true;
        function.Receiver = Arbitrum.Address.Zero;
        function.AcceptablePrice = GmxHelpers.GetAcceptablePrice(100, true);
        function.MinOut = new BigInteger(0);
        function.ExecutionFee = Web3.Convert.ToWei(_positionUpdateFees); // Fee required to execute tx
        function.WithdrawETH = false;
        function.CallbackTarget = Arbitrum.Address.Zero;

        function.AmountToSend = Web3.Convert.ToWei(_positionUpdateFees);
        function.FromAddress = Arbitrum.Address.Zero;

        var totalCost = 0m;

        try
        {
            var gasCost = await positionRouter.ContractHandler.EstimateGasAsync(function);
            var gasPrice = GetGasPrice();
            var gas = gasPrice * gasCost;
            totalCost = ethPrice * Web3.Convert.FromWei(gas, 18);
            return totalCost;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }

        return totalCost;
    }
}