import {MarketsInfoData} from "../../types/markets.js";
import {DecreasePositionSwapType, OrderType} from "../../types/orders.js";
import {TokenData, TokensData, TokensRatio} from "../../types/tokens.js";
import {DecreasePositionAmounts, FindSwapPath, SwapAmounts} from "../../types/trade.js";
import {getByKey} from "../../utils/objects.js";
import {createFindSwapPath, findAllSwapPaths, getWrappedAddress} from "../../utils/swap/swapPath.js";
import {
    convertToUsd,
    getIsEquivalentTokens,
    getIsUnwrap,
    getIsWrap,
    getTokensRatioByPrice
} from "../../utils/tokens.js";
import {getIncreasePositionAmounts} from "../../utils/trade/amounts.js";
import {getAcceptablePriceInfo, getDefaultAcceptablePriceImpactBps, getOrderThresholdType} from "../../utils/prices.js";

import type {GmxSdk} from "../..";
import {createSwapEstimator, getMarketsGraph} from "../../utils/swap/swapRouting.js";
import {getSwapAmountsByFromValue, getSwapAmountsByToValue} from "../../utils/swap/index.js";
import {EntryField, SidecarSlTpOrderEntryValid} from "../../types/sidecarOrders.js";
import {ethers} from "ethers";

/** Base Optional params for helpers, allows to avoid calling markets, tokens and uiFeeFactor methods if they are already passed */
interface BaseOptionalParams {
    marketsInfoData?: MarketsInfoData;
    tokensData?: TokensData;
    uiFeeFactor?: bigint;
}

export type PositionIncreaseParams = (
    | {
    /** Increase amounts will be calculated based on collateral amount */
    payAmount: bigint;
}
    | {
    /** Increase amounts will be calculated based on position size amount */
    sizeAmount: bigint;
}
    ) & {
    marketAddress: string;
    payTokenAddress: string;
    collateralTokenAddress: string;

    /** @default 100 */
    allowedSlippageBps?: number;
    referralCodeForTxn?: string;

    leverage?: bigint;
    /** If presented, then it's limit order */
    limitPrice?: bigint;
    acceptablePriceImpactBuffer?: number;
    fixedAcceptablePriceImpactBps?: bigint;

    skipSimulation?: boolean;
    stopLossPrice?: bigint;
    takeProfitPrice?: bigint;
} & BaseOptionalParams;

async function getAndValidateBaseParams(sdk: GmxSdk, params: BaseOptionalParams) {
    let tokensData: TokensData | undefined = params.tokensData;
    let marketsInfoData: MarketsInfoData | undefined = params.marketsInfoData;

    if (!params.marketsInfoData && !params.tokensData) {
        const result = await sdk.markets.getMarketsInfo();
        marketsInfoData = result.marketsInfoData;
        tokensData = result.tokensData;
    }

    if (!tokensData) {
        throw new Error("Tokens data is not available");
    }

    if (!marketsInfoData) {
        throw new Error("Markets info data is not available");
    }

    let uiFeeFactor = params.uiFeeFactor;
    if (!uiFeeFactor) {
        uiFeeFactor = await sdk.utils.getUiFeeFactor();
    }

    return {
        tokensData,
        marketsInfoData,
        uiFeeFactor,
    };
}

export async function increaseOrderHelper(
    sdk: GmxSdk,
    params: PositionIncreaseParams & {
        isLong: boolean;
    }
) {
    const {tokensData, marketsInfoData, uiFeeFactor} = await getAndValidateBaseParams(sdk, params);

    const isLimit = Boolean(params.limitPrice);

    const fromToken = tokensData[params.payTokenAddress];
    const collateralToken = tokensData[params.collateralTokenAddress];

    if (!fromToken) {
        throw new Error("From token is not available");
    }

    if (!collateralToken) {
        throw new Error("Collateral token is not available");
    }

    const marketInfo = getByKey(marketsInfoData, params.marketAddress);

    if (!marketInfo) {
        throw new Error("Market info is not available");
    }

    const collateralTokenAddress = collateralToken.address;
    const allowedSlippage = params.allowedSlippageBps ?? 100;

    const graph = getMarketsGraph(Object.values(marketsInfoData));
    const wrappedFromAddress = getWrappedAddress(sdk.chainId, params.payTokenAddress);
    const wrappedToAddress = getWrappedAddress(sdk.chainId, collateralTokenAddress);

    const allPaths = findAllSwapPaths({
        chainId: sdk.chainId,
        fromTokenAddress: params.payTokenAddress,
        toTokenAddress: collateralTokenAddress,
        marketsInfoData,
        graph,
        wrappedFromAddress,
        wrappedToAddress,
    });

    const estimator = createSwapEstimator(marketsInfoData);

    const findSwapPath = createFindSwapPath({
        chainId: sdk.chainId,
        fromTokenAddress: params.payTokenAddress,
        toTokenAddress: collateralTokenAddress,
        marketsInfoData,
        estimator,
        allPaths,
    });

    const payOrSizeAmount = "payAmount" in params ? params.payAmount : params.sizeAmount;

    const increaseAmounts = getIncreasePositionAmounts({
        marketInfo,
        indexToken: marketInfo.indexToken,
        initialCollateralToken: fromToken,
        collateralToken,
        isLong: params.isLong,
        initialCollateralAmount: payOrSizeAmount,
        position: undefined,
        indexTokenAmount: payOrSizeAmount,
        leverage: params.leverage,
        triggerPrice: params.limitPrice,
        limitOrderType: params.limitPrice ? OrderType.LimitIncrease : undefined,
        userReferralInfo: undefined,
        strategy: "payAmount" in params ? "leverageByCollateral" : "leverageBySize",
        findSwapPath: findSwapPath,
        uiFeeFactor,
        acceptablePriceImpactBuffer: params.acceptablePriceImpactBuffer,
        fixedAcceptablePriceImpactBps: params.fixedAcceptablePriceImpactBps,
        externalSwapQuote: undefined,
    });

    const createSltpEntries: SidecarSlTpOrderEntryValid[] = []

    let stopLossDecreaseAmounts: DecreasePositionAmounts | undefined;
    if (params.stopLossPrice) {
        const stopLossCollateralDeltaUsd = convertToUsd(increaseAmounts.collateralDeltaAmount, collateralToken.decimals, params.stopLossPrice);

        // Use a higher acceptable price impact buffer for stop loss orders to ensure execution
        const stopLossAcceptablePriceImpactBps = params.fixedAcceptablePriceImpactBps ?? 
            getDefaultAcceptablePriceImpactBps({
                isIncrease: false,
                isLong: params.isLong,
                indexPrice: params.stopLossPrice,
                sizeDeltaUsd: increaseAmounts.sizeDeltaUsd,
                priceImpactDeltaUsd: 0n, // We'll calculate this based on current market conditions
                acceptablePriceImapctBuffer: params.acceptablePriceImpactBuffer ?? 100, // Default 1% buffer for SL orders
            });

        const acceptablePriceInfo = getAcceptablePriceInfo({
            marketInfo,
            isIncrease: false,
            isLong: params.isLong,
            indexPrice: params.stopLossPrice,
            sizeDeltaUsd: increaseAmounts.sizeDeltaUsd,
            maxNegativePriceImpactBps: stopLossAcceptablePriceImpactBps,
        });

        // Calculate collateral price based on GMX UI logic
        const stopLossCollateralPrice = getIsEquivalentTokens(marketInfo.indexToken, collateralToken)
            ? params.stopLossPrice  // Use trigger price if index token equals collateral token
            : collateralToken.prices.minPrice; // Otherwise use collateral token's min price

        stopLossDecreaseAmounts = {
            isFullClose: true,
            sizeDeltaUsd: increaseAmounts.sizeDeltaUsd,
            sizeDeltaInTokens: increaseAmounts.sizeDeltaInTokens,
            collateralDeltaUsd: stopLossCollateralDeltaUsd,
            collateralDeltaAmount: increaseAmounts.collateralDeltaAmount,
            indexPrice: params.stopLossPrice,
            collateralPrice: stopLossCollateralPrice,
            acceptablePrice: params.isLong ? 0n : ethers.MaxUint256,
            acceptablePriceDeltaBps: acceptablePriceInfo.acceptablePriceDeltaBps,
            recommendedAcceptablePriceDeltaBps: 30n,
            estimatedPnl: 0n,
            estimatedPnlPercentage: 0n,
            realizedPnl: 0n,
            realizedPnlPercentage: 0n,
            positionFeeUsd: 0n,
            uiFeeUsd: 0n,
            swapUiFeeUsd: 0n,
            feeDiscountUsd: 0n,
            borrowingFeeUsd: 0n,
            fundingFeeUsd: 0n,
            swapProfitFeeUsd: 0n,
            positionPriceImpactDeltaUsd: 0n,
            priceImpactDiffUsd: acceptablePriceInfo.priceImpactDeltaUsd,
            payedRemainingCollateralAmount: 0n,
            payedOutputUsd: 0n,
            payedRemainingCollateralUsd: 0n,
            receiveTokenAmount: 0n,
            receiveUsd: 0n,
            decreaseSwapType: DecreasePositionSwapType.SwapPnlTokenToCollateralToken,
            triggerOrderType: OrderType.StopLossDecrease,
            triggerPrice: params.stopLossPrice,
            triggerThresholdType: getOrderThresholdType(OrderType.StopLossDecrease, params.isLong),
        }

        console.log(stopLossDecreaseAmounts);

        const stopLossEntry: SidecarSlTpOrderEntryValid = {
            decreaseAmounts: stopLossDecreaseAmounts,
            id: "sl-order",
            price: {
                input: params.stopLossPrice?.toString() ?? "",
                value: params.stopLossPrice,
                error: null,
            } as EntryField,
            sizeUsd: {
                input: increaseAmounts.sizeDeltaUsd.toString(),
                value: increaseAmounts.sizeDeltaUsd,
                error: null,
            } as EntryField,
            percentage: {
                input: "100",
                value: 100n,
                error: null,
            } as EntryField,
            txnType: "create",
            mode: "keepSize",
            order: null,
            increaseAmounts: undefined
        }

        createSltpEntries.push(stopLossEntry)
    }

    let takeProfitDecreaseAmounts: DecreasePositionAmounts | undefined;
    if (params.takeProfitPrice) {
        const takeProfitCollateralDeltaUsd = convertToUsd(increaseAmounts.collateralDeltaAmount, collateralToken.decimals, params.takeProfitPrice);

        // Use a higher acceptable price impact buffer for take profit orders to ensure execution
        const takeProfitAcceptablePriceImpactBps = params.fixedAcceptablePriceImpactBps ?? 
            getDefaultAcceptablePriceImpactBps({
                isIncrease: false,
                isLong: params.isLong,
                indexPrice: params.takeProfitPrice,
                sizeDeltaUsd: increaseAmounts.sizeDeltaUsd,
                priceImpactDeltaUsd: 0n, // We'll calculate this based on current market conditions
                acceptablePriceImapctBuffer: params.acceptablePriceImpactBuffer ?? 100, // Default 1% buffer for TP orders
            });

        const acceptablePriceInfo = getAcceptablePriceInfo({
            marketInfo,
            isIncrease: false,
            isLong: params.isLong,
            indexPrice: params.takeProfitPrice,
            sizeDeltaUsd: increaseAmounts.sizeDeltaUsd,
            maxNegativePriceImpactBps: takeProfitAcceptablePriceImpactBps,
        });

        // Calculate collateral price based on GMX UI logic
        const takeProfitCollateralPrice = getIsEquivalentTokens(marketInfo.indexToken, collateralToken)
            ? params.takeProfitPrice  // Use trigger price if index token equals collateral token
            : collateralToken.prices.minPrice; // Otherwise use collateral token's min price

        takeProfitDecreaseAmounts = {
            isFullClose: true,
            sizeDeltaUsd: increaseAmounts.sizeDeltaUsd,
            sizeDeltaInTokens: increaseAmounts.sizeDeltaInTokens,
            collateralDeltaUsd: takeProfitCollateralDeltaUsd,
            collateralDeltaAmount: increaseAmounts.collateralDeltaAmount,
            indexPrice: params.takeProfitPrice, // Keep original trigger price for indexPrice
            collateralPrice: takeProfitCollateralPrice,
            acceptablePrice: acceptablePriceInfo.acceptablePrice,
            acceptablePriceDeltaBps: acceptablePriceInfo.acceptablePriceDeltaBps, // Add 0.5% buffer to acceptable price impact
            recommendedAcceptablePriceDeltaBps: 30n, // Set recommended buffer to 0.5%
            estimatedPnl: 0n,
            estimatedPnlPercentage: 0n,
            realizedPnl: 0n,
            realizedPnlPercentage: 0n,
            positionFeeUsd: 0n,
            uiFeeUsd: 0n,
            swapUiFeeUsd: 0n,
            feeDiscountUsd: 0n,
            borrowingFeeUsd: 0n,
            fundingFeeUsd: 0n,
            swapProfitFeeUsd: 0n,
            positionPriceImpactDeltaUsd: 0n,
            priceImpactDiffUsd: acceptablePriceInfo.priceImpactDeltaUsd,
            payedRemainingCollateralAmount: 0n,
            payedOutputUsd: 0n,
            payedRemainingCollateralUsd: 0n,
            receiveTokenAmount: 0n,
            receiveUsd: 0n,
            decreaseSwapType: DecreasePositionSwapType.SwapPnlTokenToCollateralToken,
            triggerOrderType: OrderType.LimitDecrease,
            triggerPrice: params.takeProfitPrice,
            triggerThresholdType: getOrderThresholdType(OrderType.LimitDecrease, params.isLong),
        }

        const takeProfitEntry: SidecarSlTpOrderEntryValid = {
            decreaseAmounts: takeProfitDecreaseAmounts,
            id: "tp-order",
            price: {
                input: params.takeProfitPrice?.toString() ?? "",
                value: params.takeProfitPrice,
                error: null,
            } as EntryField,
            sizeUsd: {
                input: increaseAmounts.sizeDeltaUsd.toString(),
                value: increaseAmounts.sizeDeltaUsd,
                error: null,
            } as EntryField,
            percentage: {
                input: "100",
                value: 100n,
                error: null,
            } as EntryField,
            txnType: "create",
            mode: "keepSize",
            order: null,
            increaseAmounts: undefined
        }

        createSltpEntries.push(takeProfitEntry)
    }

    const createIncreaseOrderParams: Parameters<typeof sdk.orders.createIncreaseOrder>[0] = {
        marketsInfoData,
        tokensData,
        isLimit,
        marketAddress: params.marketAddress,
        fromToken: tokensData[params.payTokenAddress],
        allowedSlippage,
        collateralToken,
        referralCodeForTxn: params.referralCodeForTxn,
        triggerPrice: params.limitPrice,
        collateralTokenAddress: collateralToken.address,
        isLong: params.isLong,
        receiveTokenAddress: collateralTokenAddress,
        indexToken: marketInfo.indexToken,
        marketInfo,
        skipSimulation: params.skipSimulation,
        increaseAmounts,
        createSltpEntries: createSltpEntries.length > 0 ? createSltpEntries : undefined,
    };

    return sdk.orders.createIncreaseOrder(createIncreaseOrderParams);
}

function getTriggerRatio({
                             toToken,
                             fromToken,
                             triggerPrice,
                         }: {
    toToken: TokenData;
    fromToken: TokenData;
    triggerPrice: bigint;
}) {
    const fromTokenPrice = fromToken?.prices.minPrice;
    const markPrice = toToken.prices.minPrice;

    const markRatio = getTokensRatioByPrice({
        fromToken,
        toToken,
        fromPrice: fromTokenPrice,
        toPrice: markPrice,
    });

    const triggerRatio: TokensRatio = {
        ratio: triggerPrice > 0 ? triggerPrice : markRatio.ratio,
        largestToken: markRatio.largestToken,
        smallestToken: markRatio.smallestToken,
    };

    return triggerRatio;
}

export type SwapParams = (
    | {
    fromAmount: bigint;
}
    | {
    toAmount: bigint;
}
    ) & {
    fromTokenAddress: string;
    toTokenAddress: string;
    allowedSlippageBps?: number;
    referralCodeForTxn?: string;

    /** If presented, then it's limit swap order */
    triggerPrice?: bigint;
} & BaseOptionalParams;

export async function swap(sdk: GmxSdk, params: SwapParams) {
    const {tokensData, marketsInfoData, uiFeeFactor} = await getAndValidateBaseParams(sdk, params);

    const fromToken = tokensData[params.fromTokenAddress];
    const toToken = tokensData[params.toTokenAddress];

    if (!fromToken || !toToken) {
        throw new Error("From or to token is not available");
    }

    const isLimit = Boolean(params.triggerPrice);

    if (!fromToken || !toToken) {
        return undefined;
    }

    const graph = getMarketsGraph(Object.values(marketsInfoData));
    const wrappedFromAddress = getWrappedAddress(sdk.chainId, params.fromTokenAddress);
    const wrappedToAddress = getWrappedAddress(sdk.chainId, params.toTokenAddress);

    const allPaths = findAllSwapPaths({
        chainId: sdk.chainId,
        fromTokenAddress: params.fromTokenAddress,
        toTokenAddress: params.toTokenAddress,
        marketsInfoData,
        graph,
        wrappedFromAddress,
        wrappedToAddress,
    });

    const estimator = createSwapEstimator(marketsInfoData);

    const findSwapPath = createFindSwapPath({
        chainId: sdk.chainId,
        fromTokenAddress: params.fromTokenAddress,
        toTokenAddress: params.toTokenAddress,
        marketsInfoData,
        estimator,
        allPaths,
    });

    const isWrapOrUnwrap = Boolean(
        fromToken && toToken && (getIsWrap(fromToken, toToken) || getIsUnwrap(fromToken, toToken))
    );

    const swapOptimizationOrder: Parameters<FindSwapPath>[1]["order"] = isLimit ? ["length", "liquidity"] : undefined;

    let swapAmounts: SwapAmounts | undefined;

    const fromTokenPrice = fromToken.prices.minPrice;
    const triggerRatio = params.triggerPrice
        ? getTriggerRatio({
            fromToken,
            toToken,
            triggerPrice: params.triggerPrice,
        })
        : undefined;

    if (isWrapOrUnwrap) {
        const tokenAmount = "fromAmount" in params ? params.fromAmount : params.toAmount;
        const usdAmount = convertToUsd(tokenAmount, fromToken.decimals, fromTokenPrice)!;
        const price = fromTokenPrice;

        swapAmounts = {
            amountIn: tokenAmount,
            usdIn: usdAmount!,
            amountOut: tokenAmount,
            usdOut: usdAmount!,
            swapPathStats: undefined,
            priceIn: price,
            priceOut: price,
            minOutputAmount: tokenAmount,
        };

        return swapAmounts;
    } else if ("fromAmount" in params) {
        swapAmounts = getSwapAmountsByFromValue({
            tokenIn: fromToken,
            tokenOut: toToken,
            amountIn: params.fromAmount,
            triggerRatio,
            isLimit,
            findSwapPath: findSwapPath,
            uiFeeFactor,
            swapOptimizationOrder,
            allowedSwapSlippageBps: isLimit ? BigInt(params.allowedSlippageBps ?? 100) : undefined,
        });
    } else {
        swapAmounts = getSwapAmountsByToValue({
            tokenIn: fromToken,
            tokenOut: toToken,
            amountOut: params.toAmount,
            triggerRatio,
            isLimit: isLimit,
            findSwapPath: findSwapPath,
            uiFeeFactor,
            swapOptimizationOrder,
            allowedSwapSlippageBps: isLimit ? BigInt(params.allowedSlippageBps ?? 100) : undefined,
        });
    }

    if (!swapAmounts) {
        return undefined;
    }

    const createSwapOrderParams: Parameters<typeof sdk.orders.createSwapOrder>[0] = {
        tokensData,
        fromToken: tokensData[params.fromTokenAddress],
        toToken: tokensData[params.toTokenAddress],
        swapAmounts,
        isLimit,
        allowedSlippage: params.allowedSlippageBps ?? 100,
        referralCodeForTxn: params.referralCodeForTxn,
        triggerPrice: params.triggerPrice,
    };

    return sdk.orders.createSwapOrder(createSwapOrderParams);
}
