import { Address, Instruction, Slot, Option, none, TransactionSigner, lamports } from '@solana/kit';
import Decimal from 'decimal.js';
import {
  KaminoAction,
  KaminoMarket,
  KaminoObligation,
  KaminoReserve,
  lamportsToNumberDecimal as fromLamports,
  getTokenIdsForScopeRefresh,
  isKaminoObligation,
  toJson,
} from '../classes';
import { getFlashLoanInstructions } from './instructions';
import {
  assertAdjustDecreaseCollFlashCalcs,
  assertAdjustDecreaseDebtFlashCalcs,
  assertAdjustIncreaseCollFlashCalcs,
  assertAdjustIncreaseDebtFlashCalcs,
  assertDepositCollFlashCalcs,
  assertDepositDebtFlashCalcs,
  assertWithdrawCollFlashCalcs,
  assertWithdrawDebtFlashCalcs,
} from './operationGuards';

import { numberToLamportsDecimal as toLamports } from '../classes';
import {
  LeverageObligation,
  MultiplyObligation,
  ObligationType,
  ObligationTypeTag,
  SOL_DECIMALS,
  ScopePriceRefreshConfig,
  U64_MAX,
  createAtasIdempotent,
  getAssociatedTokenAddress,
  getComputeBudgetAndPriorityFeeIxs,
  getTransferWsolIxs,
  removeBudgetIxs,
  uniqueAccountsWithProgramIds,
  WRAPPED_SOL_MINT,
  MultiplyObligationFixedRate,
  LeverageObligationFixedRate,
} from '../utils';
import {
  adjustDepositLeverageCalcs,
  adjustDepositLeverageCalcsDebtFlash,
  adjustWithdrawLeverageCalcs,
  adjustWithdrawLeverageCalcsCollFlash,
  calcAdjustAmounts,
  depositLeverageCalcs,
  depositLeverageCalcsDebtFlash,
  withdrawLeverageCalcs,
  withdrawLeverageCalcsCollFlash,
} from './calcs';
import { FullBPS } from '@kamino-finance/kliquidity-sdk/dist/utils/CreationParameters';
import {
  AdjustDepositDebtFlashCalcsResult,
  AdjustLeverageCalcsResult,
  AdjustLeverageInitialInputs,
  AdjustLeverageIxsResponse,
  AdjustLeverageProps,
  AdjustLeverageSwapInputsProps,
  AdjustWithdrawCollFlashCalcsResult,
  DepositLeverageCalcsResult,
  DepositLeverageDebtFlashCalcsResult,
  DepositLeverageInitialInputs,
  DepositWithLeverageProps,
  DepositWithLeverageSwapInputsProps,
  DepositLeverageIxsResponse,
  SwapInputs,
  SwapIxs,
  SwapIxsProvider,
  WithdrawLeverageCalcsResult,
  WithdrawLeverageCollFlashCalcsResult,
  WithdrawLeverageInitialInputs,
  WithdrawLeverageIxsResponse,
  WithdrawWithLeverageProps,
  WithdrawWithLeverageSwapInputsProps,
  LeverageIxsOutput,
  FlashLoanInfo,
} from './types';
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import { findAssociatedTokenPda, getCloseAccountInstruction } from '@solana-program/token-2022';
import { LAMPORTS_PER_SOL } from '../utils/consts';

export const WITHDRAW_SLOT_OFFSET = 150; // Offset for the withdraw slot to underestimate the exchange rate

export async function getDepositWithLeverageSwapInputs<QuoteResponse>({
  owner,
  kaminoMarket,
  debtReserveAddress,
  collReserveAddress,
  depositAmount,
  priceDebtToColl,
  slippagePct,
  obligation,
  referrer,
  currentSlot,
  targetLeverage,
  selectedTokenMint,
  obligationTypeTagOverride,
  scopeRefreshIx,
  budgetAndPriorityFeeIxs,
  quoteBufferBps,
  quoter,
  useV2Ixs,
  elevationGroupOverride,
  flashBorrowType,
  logger,
}: DepositWithLeverageSwapInputsProps<QuoteResponse>): Promise<{
  flashLoanInfo: FlashLoanInfo;
  swapInputs: SwapInputs;
  initialInputs: DepositLeverageInitialInputs<QuoteResponse>;
}> {
  const log = logger ?? (() => {});
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const solTokenReserve =
    collReserve.getLiquidityMint() === WRAPPED_SOL_MINT
      ? collReserve
      : debtReserve.getLiquidityMint() === WRAPPED_SOL_MINT
      ? debtReserve
      : undefined;

  const selectedTokenIsCollToken = selectedTokenMint === collTokenMint;
  const depositTokenIsSol = !solTokenReserve ? false : selectedTokenMint === solTokenReserve.getLiquidityMint();

  const obligationType = checkObligationType(
    obligationTypeTagOverride,
    collReserve.address,
    debtReserve.address,
    kaminoMarket
  );

  const dummySwapIxs: SwapIxs<QuoteResponse>[] = [
    { preActionIxs: [], swapIxs: [], lookupTables: [], quote: { priceAInB: new Decimal(0), quoteResponse: undefined } },
  ];
  const resolvedObligation = obligation ? obligation : obligationType;

  if (flashBorrowType !== 'debt') {
    // Coll flash path (default): flash borrow coll -> deposit coll -> borrow debt -> swap debt->coll -> flash repay coll
    const flashLoanFee = collReserve.getFlashLoanFee();

    const calcs = depositLeverageCalcs({
      depositAmount: depositAmount,
      depositTokenIsCollToken: selectedTokenIsCollToken,
      depositTokenIsSol,
      priceDebtToColl,
      targetLeverage,
      slippagePct,
      flashLoanFee,
    });

    log('Deposit calcs (coll flash)', toJson(calcs));

    const klendIxs: LeverageIxsOutput = (
      await buildDepositWithLeverageIxsCollFlash(
        kaminoMarket,
        debtReserve,
        collReserve,
        owner,
        resolvedObligation,
        referrer,
        currentSlot,
        depositTokenIsSol,
        scopeRefreshIx,
        calcs,
        budgetAndPriorityFeeIxs,
        dummySwapIxs,
        useV2Ixs,
        elevationGroupOverride
      )
    )[0];

    const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
    const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();

    const swapInputsForQuote: SwapInputs = {
      inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
      inputMint: debtTokenMint,
      outputMint: collTokenMint,
    };

    const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

    const quotePriceCalcs = depositLeverageCalcs({
      depositAmount: depositAmount,
      depositTokenIsCollToken: selectedTokenIsCollToken,
      depositTokenIsSol,
      priceDebtToColl: swapQuote.priceAInB,
      targetLeverage,
      slippagePct,
      flashLoanFee,
    });

    const swapInputAmountQuotePrice = toLamports(quotePriceCalcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();

    return {
      swapInputs: {
        inputAmountLamports: swapInputAmountQuotePrice,
        minOutAmountLamports: toLamports(quotePriceCalcs.flashBorrowInCollToken, collReserve.stats.decimals),
        inputMint: debtTokenMint,
        outputMint: collTokenMint,
      },
      flashLoanInfo: klendIxs.flashLoanInfo,
      initialInputs: {
        calcs: quotePriceCalcs,
        swapQuote,
        currentSlot,
        obligation: resolvedObligation,
        klendAccounts: uniqueKlendAccounts,
      },
    };
  } else {
    // Debt flash path: flash borrow debt -> swap debt->coll -> deposit coll -> borrow debt -> flash repay debt
    const flashLoanFee = debtReserve.getFlashLoanFee();

    const calcs = depositLeverageCalcsDebtFlash({
      depositAmount,
      depositTokenIsCollToken: selectedTokenIsCollToken,
      depositTokenIsSol,
      priceDebtToColl,
      targetLeverage,
      slippagePct,
      flashLoanFee,
    });

    log('Deposit calcs (debt flash)', toJson(calcs));

    const klendIxs: LeverageIxsOutput = (
      await buildDepositWithLeverageIxsDebtFlash(
        kaminoMarket,
        debtReserve,
        collReserve,
        owner,
        resolvedObligation,
        referrer,
        currentSlot,
        depositTokenIsSol,
        scopeRefreshIx,
        calcs,
        budgetAndPriorityFeeIxs,
        dummySwapIxs,
        useV2Ixs,
        elevationGroupOverride
      )
    )[0];

    const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
    const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();

    const swapInputsForQuote: SwapInputs = {
      inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
      inputMint: debtTokenMint,
      outputMint: collTokenMint,
    };

    const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

    const quotePriceCalcs = depositLeverageCalcsDebtFlash({
      depositAmount,
      depositTokenIsCollToken: selectedTokenIsCollToken,
      depositTokenIsSol,
      priceDebtToColl: swapQuote.priceAInB,
      targetLeverage,
      slippagePct,
      flashLoanFee,
    });

    const swapInputAmountQuotePrice = toLamports(quotePriceCalcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();

    return {
      swapInputs: {
        inputAmountLamports: swapInputAmountQuotePrice,
        minOutAmountLamports: toLamports(quotePriceCalcs.swapCollTokenExpectedOut, collReserve.stats.decimals),
        inputMint: debtTokenMint,
        outputMint: collTokenMint,
      },
      flashLoanInfo: klendIxs.flashLoanInfo,
      initialInputs: {
        calcs: quotePriceCalcs,
        swapQuote,
        currentSlot,
        obligation: resolvedObligation,
        klendAccounts: uniqueKlendAccounts,
      },
    };
  }
}

export async function getDepositWithLeverageIxs<QuoteResponse>({
  owner,
  kaminoMarket,
  debtReserveAddress,
  collReserveAddress,
  depositAmount,
  priceDebtToColl,
  slippagePct,
  obligation,
  referrer,
  currentSlot,
  targetLeverage,
  selectedTokenMint,
  obligationTypeTagOverride,
  scopeRefreshIx,
  budgetAndPriorityFeeIxs,
  quoteBufferBps,
  quoter,
  swapper,
  elevationGroupOverride,
  useV2Ixs,
  rollOver,
  flashBorrowType,
  logger,
}: DepositWithLeverageProps<QuoteResponse>): Promise<Array<DepositLeverageIxsResponse<QuoteResponse>>> {
  const { swapInputs, initialInputs } = await getDepositWithLeverageSwapInputs({
    owner,
    kaminoMarket,
    debtReserveAddress,
    collReserveAddress,
    depositAmount,
    priceDebtToColl,
    slippagePct,
    obligation,
    referrer,
    currentSlot,
    targetLeverage,
    selectedTokenMint,
    obligationTypeTagOverride,
    scopeRefreshIx,
    budgetAndPriorityFeeIxs,
    quoteBufferBps,
    quoter,
    useV2Ixs,
    flashBorrowType,
    logger,
  });

  const depositSwapper: SwapIxsProvider<QuoteResponse> = swapper;

  const swapsArray = await depositSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);

  // Strategy lookup table logic removed

  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const solTokenReserve =
    collReserve.getLiquidityMint() === WRAPPED_SOL_MINT
      ? collReserve
      : debtReserve.getLiquidityMint() === WRAPPED_SOL_MINT
      ? debtReserve
      : undefined;
  const depositTokenIsSol = !solTokenReserve ? false : selectedTokenMint === solTokenReserve!.getLiquidityMint();

  const swapIxsArray = swapsArray.map((swap) => {
    return {
      preActionIxs: [] as Instruction[],
      swapIxs: swap.swapIxs,
      lookupTables: swap.lookupTables,
      quote: swap.quote,
    };
  });

  let depositWithLeverageIxs: LeverageIxsOutput[];
  const depositCalcs = initialInputs.calcs;
  if (flashBorrowType !== 'debt') {
    // Coll flash path (default): flash borrow coll -> deposit+borrow -> swap debt->coll -> flash repay coll
    assertDepositCollFlashCalcs(depositCalcs);
    depositWithLeverageIxs = await buildDepositWithLeverageIxsCollFlash(
      kaminoMarket,
      debtReserve!,
      collReserve!,
      owner,
      initialInputs.obligation,
      referrer,
      currentSlot,
      depositTokenIsSol,
      scopeRefreshIx,
      depositCalcs,
      budgetAndPriorityFeeIxs,
      swapIxsArray,
      useV2Ixs,
      elevationGroupOverride,
      rollOver
    );
  } else {
    // Debt flash path: flash borrow debt -> swap debt->coll -> deposit+borrow -> flash repay debt
    assertDepositDebtFlashCalcs(depositCalcs);
    depositWithLeverageIxs = await buildDepositWithLeverageIxsDebtFlash(
      kaminoMarket,
      debtReserve!,
      collReserve!,
      owner,
      initialInputs.obligation,
      referrer,
      currentSlot,
      depositTokenIsSol,
      scopeRefreshIx,
      depositCalcs,
      budgetAndPriorityFeeIxs,
      swapIxsArray,
      useV2Ixs,
      elevationGroupOverride
    );
  }

  return depositWithLeverageIxs.map((depositWithLeverageIxs, index) => {
    return {
      ixs: depositWithLeverageIxs.instructions,
      flashLoanInfo: depositWithLeverageIxs.flashLoanInfo,
      lookupTables: swapsArray[index].lookupTables,
      swapInputs,
      initialInputs,
      quote: swapsArray[index].quote.quoteResponse,
    };
  });
}

async function buildDepositWithLeverageIxsCollFlash<QuoteResponse>(
  market: KaminoMarket,
  debtReserve: KaminoReserve,
  collReserve: KaminoReserve,
  owner: TransactionSigner,
  obligation: KaminoObligation | ObligationType | undefined,
  referrer: Option<Address>,
  currentSlot: Slot,
  depositTokenIsSol: boolean,
  scopeRefreshIx: Instruction[],
  calcs: DepositLeverageCalcsResult,
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  useV2Ixs: boolean,
  elevationGroupOverride?: number,
  rollOver?: boolean
): Promise<LeverageIxsOutput[]> {
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const [[collTokenAta]] = await Promise.all([
    findAssociatedTokenPda({
      owner: owner.address,
      mint: collTokenMint,
      tokenProgram: collReserve.getLiquidityTokenProgram(),
    }),
  ]);

  // 1. Create atas & budget ixs
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  const fillWsolAtaIxs: Instruction[] = [];
  if (depositTokenIsSol) {
    fillWsolAtaIxs.push(
      ...getTransferWsolIxs(
        owner,
        await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address),
        lamports(BigInt(toLamports(calcs.initDepositInSol, SOL_DECIMALS).ceil().toString()))
      )
    );
  }

  // 2. Flash borrow & repay the collateral amount needed for given leverage
  // if user deposits coll, then we borrow the diff, else we borrow the entire amount
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await market.getLendingMarketAuthority(),
    lendingMarketAddress: market.getAddress(),
    reserve: collReserve,
    amountLamports: toLamports(calcs.flashBorrowInCollToken, collReserve.stats.decimals),
    destinationAta: collTokenAta,
    // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: market.programId,
  });

  // 3. Deposit initial tokens + borrowed tokens into reserve
  const kaminoDepositAndBorrowAction = await KaminoAction.buildDepositAndBorrowTxns({
    kaminoMarket: market,
    depositAmount: toLamports(calcs.collTokenToDeposit, collReserve.stats.decimals).floor().toString(),
    depositReserveAddress: collReserve.address,
    borrowAmount: toLamports(calcs.debtTokenToBorrow, debtReserve.stats.decimals).ceil().toString(),
    borrowReserveAddress: debtReserve.address,
    owner,
    obligation: obligation!,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: elevationGroupOverride === 0 ? false : true, // emode
    initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and created in a setup tx in the UI
    referrer,
    currentSlot,
    rollOver,
  });

  return swapQuoteIxsArray.map((swapQuoteIxs) => {
    // 4. Swap
    const { swapIxs } = swapQuoteIxs;
    const swapInstructions = removeBudgetIxs(swapIxs);
    const flashBorrowReserve = collReserve;
    const flashLoanInfo = {
      flashBorrowReserve: flashBorrowReserve.address,
      flashLoanFee: flashBorrowReserve.getFlashLoanFee(),
    };

    return {
      flashLoanInfo,
      instructions: [
        ...scopeRefreshIx,
        ...createAtasIxs,
        ...fillWsolAtaIxs,
        ...[flashBorrowIx],
        ...KaminoAction.actionToIxs(kaminoDepositAndBorrowAction),
        ...swapInstructions,
        ...[flashRepayIx],
        ...budgetIxs,
      ],
    };
  });
}

/**
 * Deposit with flash borrow DEBT token.
 * Order: scopeRefresh → createAtas → fillWsol → flashBorrow(DEBT) → swap(debt→coll) → deposit+borrow → flashRepay(DEBT) → budget
 */
async function buildDepositWithLeverageIxsDebtFlash<QuoteResponse>(
  market: KaminoMarket,
  debtReserve: KaminoReserve,
  collReserve: KaminoReserve,
  owner: TransactionSigner,
  obligation: KaminoObligation | ObligationType | undefined,
  referrer: Option<Address>,
  currentSlot: Slot,
  depositTokenIsSol: boolean,
  scopeRefreshIx: Instruction[],
  calcs: DepositLeverageDebtFlashCalcsResult,
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  useV2Ixs: boolean,
  elevationGroupOverride?: number
): Promise<LeverageIxsOutput[]> {
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const [debtTokenAta] = await findAssociatedTokenPda({
    owner: owner.address,
    mint: debtTokenMint,
    tokenProgram: debtReserve.getLiquidityTokenProgram(),
  });

  // 1. Create atas & budget ixs
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  const fillWsolAtaIxs: Instruction[] = [];
  if (depositTokenIsSol) {
    fillWsolAtaIxs.push(
      ...getTransferWsolIxs(
        owner,
        await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address),
        lamports(BigInt(toLamports(calcs.initDepositInSol, SOL_DECIMALS).ceil().toString()))
      )
    );
  }

  // 2. Flash borrow DEBT token
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await market.getLendingMarketAuthority(),
    lendingMarketAddress: market.getAddress(),
    reserve: debtReserve,
    amountLamports: toLamports(calcs.flashBorrowInDebtToken, debtReserve.stats.decimals),
    destinationAta: debtTokenAta,
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: market.programId,
  });

  // 3. Deposit coll + borrow debt
  const kaminoDepositAndBorrowAction = await KaminoAction.buildDepositAndBorrowTxns({
    kaminoMarket: market,
    depositAmount: toLamports(calcs.collTokenToDeposit, collReserve.stats.decimals).floor().toString(),
    depositReserveAddress: collReserve.address,
    borrowAmount: toLamports(calcs.debtTokenToBorrow, debtReserve.stats.decimals).ceil().toString(),
    borrowReserveAddress: debtReserve.address,
    owner,
    obligation: obligation!,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: elevationGroupOverride === 0 ? false : true,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
    currentSlot,
  });

  return swapQuoteIxsArray.map((swapQuoteIxs) => {
    const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);
    const flashBorrowReserve = debtReserve;
    const flashLoanInfo = {
      flashBorrowReserve: flashBorrowReserve.address,
      flashLoanFee: flashBorrowReserve.getFlashLoanFee(),
    };

    // Key difference: swap BEFORE deposit+borrow (we need the coll from the swap to deposit)
    return {
      flashLoanInfo,
      instructions: [
        ...scopeRefreshIx,
        ...createAtasIxs,
        ...fillWsolAtaIxs,
        ...[flashBorrowIx],
        ...swapInstructions,
        ...KaminoAction.actionToIxs(kaminoDepositAndBorrowAction),
        ...[flashRepayIx],
        ...budgetIxs,
      ],
    };
  });
}

export async function getWithdrawWithLeverageSwapInputs<QuoteResponse>({
  owner,
  kaminoMarket,
  debtReserveAddress,
  collReserveAddress,
  deposited,
  borrowed,
  obligation,
  referrer,
  currentSlot,
  withdrawAmount,
  priceCollToDebt,
  slippagePct,
  isClosingPosition,
  selectedTokenMint,
  budgetAndPriorityFeeIxs,
  scopeRefreshIx,
  quoteBufferBps,
  quoter,
  useV2Ixs,
  userSolBalanceLamports,
  flashBorrowType,
  logger,
}: WithdrawWithLeverageSwapInputsProps<QuoteResponse>): Promise<{
  swapInputs: SwapInputs;
  flashLoanInfo: FlashLoanInfo;
  initialInputs: WithdrawLeverageInitialInputs<QuoteResponse>;
}> {
  const log = logger ?? (() => {});
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const selectedTokenIsCollToken = selectedTokenMint === collTokenMint;
  const inputTokenIsSol = selectedTokenMint === WRAPPED_SOL_MINT;

  const dummySwapIxs: SwapIxs<QuoteResponse>[] = [
    { preActionIxs: [], swapIxs: [], lookupTables: [], quote: { priceAInB: new Decimal(0), quoteResponse: undefined } },
  ];

  if (flashBorrowType !== 'coll') {
    // Debt flash path (default): flash borrow debt -> repay+withdraw -> swap coll->debt -> flash repay debt
    const flashLoanFee = debtReserve!.getFlashLoanFee();

    const calcs = withdrawLeverageCalcs(
      kaminoMarket,
      collReserve!,
      debtReserve!,
      priceCollToDebt,
      withdrawAmount,
      deposited,
      borrowed,
      currentSlot,
      isClosingPosition,
      selectedTokenIsCollToken,
      selectedTokenMint,
      obligation,
      flashLoanFee,
      slippagePct
    );

    log('Withdraw calcs (debt flash)', toJson(calcs));

    const klendIxs = (
      await buildWithdrawWithLeverageIxsDebtFlash(
        kaminoMarket,
        debtReserve!,
        collReserve!,
        owner,
        obligation,
        referrer,
        currentSlot,
        isClosingPosition,
        inputTokenIsSol,
        scopeRefreshIx,
        calcs,
        budgetAndPriorityFeeIxs,
        dummySwapIxs,
        useV2Ixs,
        userSolBalanceLamports
      )
    )[0];

    const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
    const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve!.getMintDecimals()).ceil();

    const swapInputsForQuote: SwapInputs = {
      inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
      inputMint: collTokenMint,
      outputMint: debtTokenMint,
    };

    const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

    const calcsQuotePrice = withdrawLeverageCalcs(
      kaminoMarket,
      collReserve!,
      debtReserve!,
      swapQuote.priceAInB,
      withdrawAmount,
      deposited,
      borrowed,
      currentSlot,
      isClosingPosition,
      selectedTokenIsCollToken,
      selectedTokenMint,
      obligation,
      flashLoanFee,
      slippagePct
    );

    const swapInputAmountQuotePrice = toLamports(
      calcsQuotePrice.collTokenSwapIn,
      collReserve!.getMintDecimals()
    ).ceil();

    return {
      swapInputs: {
        inputAmountLamports: swapInputAmountQuotePrice,
        minOutAmountLamports: calcsQuotePrice.repayAmount,
        inputMint: collTokenMint,
        outputMint: debtTokenMint,
      },
      flashLoanInfo: klendIxs.flashLoanInfo,
      initialInputs: {
        calcs: calcsQuotePrice,
        swapQuote,
        currentSlot,
        obligation,
        klendAccounts: uniqueKlendAccounts,
      },
    };
  } else {
    // Coll flash path: flash borrow coll -> swap coll->debt -> repay+withdraw -> flash repay coll
    const flashLoanFee = collReserve!.getFlashLoanFee();

    const calcs = withdrawLeverageCalcsCollFlash(
      kaminoMarket,
      collReserve!,
      debtReserve!,
      priceCollToDebt,
      withdrawAmount,
      deposited,
      borrowed,
      currentSlot,
      isClosingPosition,
      selectedTokenIsCollToken,
      selectedTokenMint,
      obligation,
      flashLoanFee,
      slippagePct
    );

    log('Withdraw calcs (coll flash)', toJson(calcs));

    const klendIxs = (
      await buildWithdrawWithLeverageIxsCollFlash(
        kaminoMarket,
        debtReserve!,
        collReserve!,
        owner,
        obligation,
        referrer,
        currentSlot,
        isClosingPosition,
        inputTokenIsSol,
        scopeRefreshIx,
        calcs,
        budgetAndPriorityFeeIxs,
        dummySwapIxs,
        useV2Ixs,
        userSolBalanceLamports
      )
    )[0];

    const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
    const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve!.getMintDecimals()).ceil();

    const swapInputsForQuote: SwapInputs = {
      inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
      inputMint: collTokenMint,
      outputMint: debtTokenMint,
    };

    const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

    const calcsQuotePrice = withdrawLeverageCalcsCollFlash(
      kaminoMarket,
      collReserve!,
      debtReserve!,
      swapQuote.priceAInB,
      withdrawAmount,
      deposited,
      borrowed,
      currentSlot,
      isClosingPosition,
      selectedTokenIsCollToken,
      selectedTokenMint,
      obligation,
      flashLoanFee,
      slippagePct
    );

    const swapInputAmountQuotePrice = toLamports(
      calcsQuotePrice.collTokenSwapIn,
      collReserve!.getMintDecimals()
    ).ceil();

    return {
      swapInputs: {
        inputAmountLamports: swapInputAmountQuotePrice,
        minOutAmountLamports: calcsQuotePrice.repayAmount,
        inputMint: collTokenMint,
        outputMint: debtTokenMint,
      },
      flashLoanInfo: klendIxs.flashLoanInfo,
      initialInputs: {
        calcs: calcsQuotePrice,
        swapQuote,
        currentSlot,
        obligation,
        klendAccounts: uniqueKlendAccounts,
      },
    };
  }
}

export async function getWithdrawWithLeverageIxs<QuoteResponse>({
  owner,
  kaminoMarket,
  debtReserveAddress,
  collReserveAddress,
  obligation,
  deposited,
  borrowed,
  referrer,
  currentSlot,
  withdrawAmount,
  priceCollToDebt,
  slippagePct,
  isClosingPosition,
  selectedTokenMint,
  budgetAndPriorityFeeIxs,
  scopeRefreshIx,
  quoteBufferBps,
  quoter,
  swapper,
  useV2Ixs,
  userSolBalanceLamports,
  flashBorrowType,
}: WithdrawWithLeverageProps<QuoteResponse>): Promise<Array<WithdrawLeverageIxsResponse<QuoteResponse>>> {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);

  const inputTokenIsSol = selectedTokenMint === WRAPPED_SOL_MINT;
  const { swapInputs, initialInputs } = await getWithdrawWithLeverageSwapInputs({
    owner,
    kaminoMarket,
    debtReserveAddress,
    collReserveAddress,
    deposited,
    borrowed,
    obligation,
    referrer,
    currentSlot,
    withdrawAmount,
    priceCollToDebt,
    slippagePct,
    isClosingPosition,
    selectedTokenMint,
    budgetAndPriorityFeeIxs,
    scopeRefreshIx,
    quoteBufferBps,
    quoter,
    useV2Ixs,
    userSolBalanceLamports,
    flashBorrowType,
  });

  const withdrawSwapper: SwapIxsProvider<QuoteResponse> = swapper;

  const swapsArray = await withdrawSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);

  // Strategy lookup table logic removed

  const swapIxsArray = swapsArray.map((swap) => {
    return {
      preActionIxs: [] as Instruction[],
      swapIxs: swap.swapIxs,
      lookupTables: swap.lookupTables,
      quote: swap.quote,
    };
  });

  let withdrawWithLeverageIxs: LeverageIxsOutput[];
  const withdrawCalcs = initialInputs.calcs;
  if (flashBorrowType !== 'coll') {
    // Debt flash path (default): flash borrow debt -> repay+withdraw -> swap coll->debt -> flash repay debt
    assertWithdrawDebtFlashCalcs(withdrawCalcs);
    withdrawWithLeverageIxs = await buildWithdrawWithLeverageIxsDebtFlash<QuoteResponse>(
      kaminoMarket,
      debtReserve!,
      collReserve!,
      owner,
      obligation,
      referrer,
      currentSlot,
      isClosingPosition,
      inputTokenIsSol,
      scopeRefreshIx,
      withdrawCalcs,
      budgetAndPriorityFeeIxs,
      swapIxsArray,
      useV2Ixs,
      userSolBalanceLamports
    );
  } else {
    // Coll flash path: flash borrow coll -> swap coll->debt -> repay+withdraw -> flash repay coll
    assertWithdrawCollFlashCalcs(withdrawCalcs);
    withdrawWithLeverageIxs = await buildWithdrawWithLeverageIxsCollFlash<QuoteResponse>(
      kaminoMarket,
      debtReserve!,
      collReserve!,
      owner,
      obligation,
      referrer,
      currentSlot,
      isClosingPosition,
      inputTokenIsSol,
      scopeRefreshIx,
      withdrawCalcs,
      budgetAndPriorityFeeIxs,
      swapIxsArray,
      useV2Ixs,
      userSolBalanceLamports
    );
  }

  // Send ixs and lookup tables
  return withdrawWithLeverageIxs.map((ixs, index) => {
    return {
      ixs: ixs.instructions,
      flashLoanInfo: ixs.flashLoanInfo,
      lookupTables: swapsArray[index].lookupTables,
      swapInputs,
      initialInputs: initialInputs,
      quote: swapsArray[index].quote.quoteResponse,
    };
  });
}

export async function buildWithdrawWithLeverageIxsDebtFlash<QuoteResponse>(
  market: KaminoMarket,
  debtReserve: KaminoReserve,
  collReserve: KaminoReserve,
  owner: TransactionSigner,
  obligation: KaminoObligation,
  referrer: Option<Address>,
  currentSlot: Slot,
  isClosingPosition: boolean,
  depositTokenIsSol: boolean,
  scopeRefreshIx: Instruction[],
  calcs: WithdrawLeverageCalcsResult,
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  useV2Ixs: boolean,
  userSolBalanceLamports: number
): Promise<LeverageIxsOutput[]> {
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const debtTokenAta = await getAssociatedTokenAddress(
    debtTokenMint,
    owner.address,
    debtReserve.getLiquidityTokenProgram()
  );
  // 1. Create atas & budget txns & user metadata

  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  const closeWsolAtaIxs: Instruction[] = [];
  if (depositTokenIsSol || debtTokenMint === WRAPPED_SOL_MINT) {
    const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address);
    closeWsolAtaIxs.push(
      getCloseAccountInstruction(
        {
          owner,
          destination: owner.address,
          account: wsolAta,
        },
        { programAddress: TOKEN_PROGRAM_ADDRESS }
      )
    );
  }

  // TODO: Mihai/Marius check if we can improve this logic and not convert any SOL
  // This is here so that we have enough wsol to repay in case the kAB swapped to sol after estimates is not enough
  const fillWsolAtaIxs: Instruction[] = [];
  if (debtTokenMint === WRAPPED_SOL_MINT) {
    const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2;
    const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1;
    fillWsolAtaIxs.push(
      ...getTransferWsolIxs(
        owner,
        await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address),
        lamports(BigInt(toLamports(balanceToWrap, SOL_DECIMALS).ceil().toString()))
      )
    );
  }

  // 2. Prepare the flash borrow and flash repay amounts and ixs
  // We borrow exactly how much we need to repay
  // and repay that + flash amount fee
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await market.getLendingMarketAuthority(),
    lendingMarketAddress: market.getAddress(),
    reserve: debtReserve!,
    amountLamports: toLamports(calcs.repayAmount, debtReserve!.stats.decimals),
    destinationAta: debtTokenAta,
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: market.programId,
  });

  // 3. Repay borrowed tokens and Withdraw tokens from reserve that will be swapped to repay flash loan
  const repayAndWithdrawAction = await KaminoAction.buildRepayAndWithdrawTxns({
    kaminoMarket: market,
    repayAmount: isClosingPosition
      ? U64_MAX
      : toLamports(calcs.repayAmount, debtReserve.stats.decimals).floor().toString(),
    repayReserveAddress: debtReserve.address,
    withdrawAmount: isClosingPosition
      ? U64_MAX
      : toLamports(calcs.depositTokenWithdrawAmount, collReserve!.stats.decimals).ceil().toString(),
    withdrawReserveAddress: collReserve.address,
    payer: owner,
    currentSlot,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and created in a setup tx in the UI (won't be the case for withdraw anyway as this would be created in deposit)
    referrer,
  });

  return swapQuoteIxsArray.map((swapQuoteIxs) => {
    const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);

    return {
      flashLoanInfo: {
        flashLoanFee: debtReserve.getFlashLoanFee(),
        flashBorrowReserve: debtReserve.address,
      },
      instructions: [
        ...scopeRefreshIx,
        ...createAtasIxs,
        ...fillWsolAtaIxs,
        ...[flashBorrowIx],
        ...KaminoAction.actionToIxs(repayAndWithdrawAction),
        ...swapInstructions,
        ...[flashRepayIx],
        ...closeWsolAtaIxs,
        ...budgetIxs,
      ],
    };
  });
}

/**
 * Withdraw with flash borrow COLLATERAL token.
 * Order: scopeRefresh → createAtas → fillWsol → flashBorrow(COLL) → swap(coll→debt) → repay+withdraw → flashRepay(COLL) → closeWsol → budget
 */
async function buildWithdrawWithLeverageIxsCollFlash<QuoteResponse>(
  market: KaminoMarket,
  debtReserve: KaminoReserve,
  collReserve: KaminoReserve,
  owner: TransactionSigner,
  obligation: KaminoObligation,
  referrer: Option<Address>,
  currentSlot: Slot,
  isClosingPosition: boolean,
  depositTokenIsSol: boolean,
  scopeRefreshIx: Instruction[],
  calcs: WithdrawLeverageCollFlashCalcsResult,
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  useV2Ixs: boolean,
  userSolBalanceLamports: number
): Promise<LeverageIxsOutput[]> {
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const [collTokenAta] = await findAssociatedTokenPda({
    owner: owner.address,
    mint: collTokenMint,
    tokenProgram: collReserve.getLiquidityTokenProgram(),
  });

  // 1. Create atas & budget ixs
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  const closeWsolAtaIxs: Instruction[] = [];
  if (depositTokenIsSol || collTokenMint === WRAPPED_SOL_MINT) {
    const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address);
    closeWsolAtaIxs.push(
      getCloseAccountInstruction(
        {
          owner,
          destination: owner.address,
          account: wsolAta,
        },
        { programAddress: TOKEN_PROGRAM_ADDRESS }
      )
    );
  }

  const fillWsolAtaIxs: Instruction[] = [];
  if (collTokenMint === WRAPPED_SOL_MINT) {
    const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2;
    const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1;
    fillWsolAtaIxs.push(
      ...getTransferWsolIxs(
        owner,
        await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address),
        lamports(BigInt(toLamports(balanceToWrap, SOL_DECIMALS).ceil().toString()))
      )
    );
  }

  // 2. Flash borrow COLL token
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await market.getLendingMarketAuthority(),
    lendingMarketAddress: market.getAddress(),
    reserve: collReserve,
    amountLamports: toLamports(calcs.flashBorrowInCollToken, collReserve.stats.decimals),
    destinationAta: collTokenAta,
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: market.programId,
  });

  // 3. Repay debt + withdraw coll
  const repayAndWithdrawAction = await KaminoAction.buildRepayAndWithdrawTxns({
    kaminoMarket: market,
    repayAmount: isClosingPosition
      ? U64_MAX
      : toLamports(calcs.repayAmount, debtReserve.stats.decimals).floor().toString(),
    repayReserveAddress: debtReserve.address,
    withdrawAmount: isClosingPosition
      ? U64_MAX
      : toLamports(calcs.depositTokenWithdrawAmount, collReserve.stats.decimals).ceil().toString(),
    withdrawReserveAddress: collReserve.address,
    payer: owner,
    currentSlot,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
  });

  return swapQuoteIxsArray.map((swapQuoteIxs) => {
    const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);

    // Key difference: swap BEFORE repay+withdraw (we need the debt from the swap to repay)
    return {
      flashLoanInfo: {
        flashLoanFee: collReserve.getFlashLoanFee(),
        flashBorrowReserve: collReserve.address,
      },
      instructions: [
        ...scopeRefreshIx,
        ...createAtasIxs,
        ...fillWsolAtaIxs,
        ...[flashBorrowIx],
        ...swapInstructions,
        ...KaminoAction.actionToIxs(repayAndWithdrawAction),
        ...[flashRepayIx],
        ...closeWsolAtaIxs,
        ...budgetIxs,
      ],
    };
  });
}

export async function getAdjustLeverageSwapInputs<QuoteResponse>({
  owner,
  kaminoMarket,
  debtReserveAddress,
  collReserveAddress,
  obligation,
  depositedLamports,
  borrowedLamports,
  referrer,
  currentSlot,
  targetLeverage,
  priceCollToDebt,
  priceDebtToColl,
  slippagePct,
  budgetAndPriorityFeeIxs,
  scopeRefreshIx,
  quoteBufferBps,
  quoter,
  useV2Ixs,
  withdrawSlotOffset,
  userSolBalanceLamports,
  flashBorrowType,
}: AdjustLeverageSwapInputsProps<QuoteResponse>): Promise<{
  swapInputs: SwapInputs;
  flashLoanInfo: FlashLoanInfo;
  initialInputs: AdjustLeverageInitialInputs<QuoteResponse>;
}> {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const deposited = fromLamports(depositedLamports, collReserve.stats.decimals);
  const borrowed = fromLamports(borrowedLamports, debtReserve.stats.decimals);

  // Getting current flash loan fee from the reserve we will flash borrow from
  const currentLeverage = obligation.refreshedStats.leverage;
  const isDepositViaLeverage = targetLeverage.gte(new Decimal(currentLeverage));
  // Determine effective flash borrow type: default is coll for increase, debt for decrease
  const effectiveFlashBorrowType = flashBorrowType || (isDepositViaLeverage ? 'coll' : 'debt');
  const flashLoanFee =
    effectiveFlashBorrowType === 'coll' ? collReserve.getFlashLoanFee() : debtReserve.getFlashLoanFee();

  const { adjustDepositPosition, adjustBorrowPosition } = calcAdjustAmounts({
    currentDepositPosition: deposited,
    currentBorrowPosition: borrowed,
    targetLeverage: targetLeverage,
    priceCollToDebt: priceCollToDebt,
    flashLoanFee: new Decimal(flashLoanFee),
  });

  const isDeposit = adjustDepositPosition.gte(0) && adjustBorrowPosition.gte(0);
  if (isDepositViaLeverage !== isDeposit) {
    throw new Error('Invalid target leverage');
  }

  if (isDeposit) {
    const dummySwapIxs: SwapIxs<QuoteResponse>[] = [
      {
        preActionIxs: [],
        swapIxs: [],
        lookupTables: [],
        quote: { priceAInB: new Decimal(0), quoteResponse: undefined },
      },
    ];

    if (effectiveFlashBorrowType !== 'debt') {
      // Coll flash path (default): flash borrow coll -> deposit -> borrow -> swap debt->coll -> flash repay coll
      const calcs = adjustDepositLeverageCalcs(
        debtReserve!,
        adjustDepositPosition,
        adjustBorrowPosition,
        priceDebtToColl,
        flashLoanFee,
        slippagePct
      );

      const klendIxs: LeverageIxsOutput = (
        await buildIncreaseLeverageIxsCollFlash(
          owner,
          kaminoMarket,
          collReserveAddress,
          debtReserveAddress,
          obligation,
          referrer,
          currentSlot,
          calcs,
          scopeRefreshIx,
          dummySwapIxs,
          budgetAndPriorityFeeIxs,
          useV2Ixs
        )
      )[0];

      const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
      const swapInputAmount = toLamports(calcs.borrowAmount, debtReserve.stats.decimals).ceil();

      const swapInputsForQuote: SwapInputs = {
        inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
        inputMint: debtTokenMint,
        outputMint: collTokenMint,
      };

      const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

      const {
        adjustDepositPosition: adjustDepositPositionQuotePrice,
        adjustBorrowPosition: adjustBorrowPositionQuotePrice,
      } = calcAdjustAmounts({
        currentDepositPosition: deposited,
        currentBorrowPosition: borrowed,
        targetLeverage,
        priceCollToDebt: new Decimal(1).div(swapQuote.priceAInB),
        flashLoanFee: new Decimal(flashLoanFee),
      });

      const calcsQuotePrice = adjustDepositLeverageCalcs(
        debtReserve,
        adjustDepositPositionQuotePrice,
        adjustBorrowPositionQuotePrice,
        swapQuote.priceAInB,
        flashLoanFee,
        slippagePct
      );

      const swapInputAmountQuotePrice = toLamports(calcsQuotePrice.borrowAmount, debtReserve.getMintDecimals()).ceil();

      return {
        swapInputs: {
          inputAmountLamports: swapInputAmountQuotePrice,
          minOutAmountLamports: toLamports(calcsQuotePrice.adjustDepositPosition, collReserve.stats.decimals),
          inputMint: debtTokenMint,
          outputMint: collTokenMint,
        },
        flashLoanInfo: klendIxs.flashLoanInfo,
        initialInputs: {
          calcs: calcsQuotePrice,
          swapQuote,
          currentSlot,
          obligation,
          klendAccounts: uniqueKlendAccounts,
          isDeposit,
        },
      };
    } else {
      // Debt flash path: flash borrow debt -> swap debt->coll -> deposit -> borrow -> flash repay debt
      const calcs = adjustDepositLeverageCalcsDebtFlash(
        debtReserve!,
        adjustDepositPosition,
        adjustBorrowPosition,
        priceDebtToColl,
        flashLoanFee,
        slippagePct
      );

      const klendIxs: LeverageIxsOutput = (
        await buildIncreaseLeverageIxsDebtFlash(
          owner,
          kaminoMarket,
          collReserveAddress,
          debtReserveAddress,
          obligation,
          referrer,
          currentSlot,
          calcs,
          scopeRefreshIx,
          dummySwapIxs,
          budgetAndPriorityFeeIxs,
          useV2Ixs
        )
      )[0];

      const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
      const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();

      const swapInputsForQuote: SwapInputs = {
        inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
        inputMint: debtTokenMint,
        outputMint: collTokenMint,
      };

      const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

      const {
        adjustDepositPosition: adjustDepositPositionQuotePrice,
        adjustBorrowPosition: adjustBorrowPositionQuotePrice,
      } = calcAdjustAmounts({
        currentDepositPosition: deposited,
        currentBorrowPosition: borrowed,
        targetLeverage,
        priceCollToDebt: new Decimal(1).div(swapQuote.priceAInB),
        flashLoanFee: new Decimal(flashLoanFee),
      });

      const calcsQuotePrice = adjustDepositLeverageCalcsDebtFlash(
        debtReserve,
        adjustDepositPositionQuotePrice,
        adjustBorrowPositionQuotePrice,
        swapQuote.priceAInB,
        flashLoanFee,
        slippagePct
      );

      const swapInputAmountQuotePrice = toLamports(
        calcsQuotePrice.swapDebtTokenIn,
        debtReserve.getMintDecimals()
      ).ceil();

      return {
        swapInputs: {
          inputAmountLamports: swapInputAmountQuotePrice,
          minOutAmountLamports: toLamports(calcsQuotePrice.adjustDepositPosition, collReserve.stats.decimals),
          inputMint: debtTokenMint,
          outputMint: collTokenMint,
        },
        flashLoanInfo: klendIxs.flashLoanInfo,
        initialInputs: {
          calcs: calcsQuotePrice,
          swapQuote,
          currentSlot,
          obligation,
          klendAccounts: uniqueKlendAccounts,
          isDeposit,
        },
      };
    }
  } else {
    const dummySwapIxs: SwapIxs<QuoteResponse>[] = [
      {
        preActionIxs: [],
        swapIxs: [],
        lookupTables: [],
        quote: { priceAInB: new Decimal(0), quoteResponse: undefined },
      },
    ];

    if (effectiveFlashBorrowType !== 'coll') {
      // Debt flash path (default): flash borrow debt -> repay -> withdraw -> swap coll->debt -> flash repay debt
      const calcs = adjustWithdrawLeverageCalcs(adjustDepositPosition, adjustBorrowPosition, flashLoanFee, slippagePct);

      const klendIxs: LeverageIxsOutput = (
        await buildDecreaseLeverageIxsDebtFlash(
          owner,
          kaminoMarket,
          collReserveAddress,
          debtReserveAddress,
          obligation,
          referrer,
          currentSlot,
          calcs,
          scopeRefreshIx,
          dummySwapIxs,
          budgetAndPriorityFeeIxs,
          useV2Ixs,
          withdrawSlotOffset,
          userSolBalanceLamports
        )
      )[0];

      const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
      const swapInputAmount = toLamports(
        calcs.withdrawAmountWithSlippageAndFlashLoanFee,
        collReserve.state.liquidity.mintDecimals.toNumber()
      ).ceil();

      const swapInputsForQuote: SwapInputs = {
        inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
        inputMint: collTokenMint,
        outputMint: debtTokenMint,
      };

      const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

      const {
        adjustDepositPosition: adjustDepositPositionQuotePrice,
        adjustBorrowPosition: adjustBorrowPositionQuotePrice,
      } = calcAdjustAmounts({
        currentDepositPosition: deposited,
        currentBorrowPosition: borrowed,
        targetLeverage,
        priceCollToDebt: swapQuote.priceAInB,
        flashLoanFee: new Decimal(flashLoanFee),
      });

      const calcsQuotePrice = adjustWithdrawLeverageCalcs(
        adjustDepositPositionQuotePrice,
        adjustBorrowPositionQuotePrice,
        flashLoanFee,
        slippagePct
      );

      const swapInputAmountQuotePrice = toLamports(
        calcsQuotePrice.withdrawAmountWithSlippageAndFlashLoanFee,
        collReserve.getMintDecimals()
      ).ceil();

      return {
        swapInputs: {
          inputAmountLamports: swapInputAmountQuotePrice,
          minOutAmountLamports: toLamports(calcsQuotePrice.adjustBorrowPosition.abs(), debtReserve.stats.decimals),
          inputMint: collTokenMint,
          outputMint: debtTokenMint,
        },
        flashLoanInfo: klendIxs.flashLoanInfo,
        initialInputs: {
          calcs: calcsQuotePrice,
          swapQuote,
          currentSlot,
          obligation,
          klendAccounts: uniqueKlendAccounts,
          isDeposit,
        },
      };
    } else {
      // Coll flash path: flash borrow coll -> swap coll->debt -> repay -> withdraw -> flash repay coll
      const calcs = adjustWithdrawLeverageCalcsCollFlash(
        adjustDepositPosition,
        adjustBorrowPosition,
        priceCollToDebt,
        flashLoanFee,
        slippagePct
      );

      const klendIxs: LeverageIxsOutput = (
        await buildDecreaseLeverageIxsCollFlash(
          owner,
          kaminoMarket,
          collReserveAddress,
          debtReserveAddress,
          obligation,
          referrer,
          currentSlot,
          calcs,
          scopeRefreshIx,
          dummySwapIxs,
          budgetAndPriorityFeeIxs,
          useV2Ixs,
          withdrawSlotOffset,
          userSolBalanceLamports
        )
      )[0];

      const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
      const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve.getMintDecimals()).ceil();

      const swapInputsForQuote: SwapInputs = {
        inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
        inputMint: collTokenMint,
        outputMint: debtTokenMint,
      };

      const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);

      const {
        adjustDepositPosition: adjustDepositPositionQuotePrice,
        adjustBorrowPosition: adjustBorrowPositionQuotePrice,
      } = calcAdjustAmounts({
        currentDepositPosition: deposited,
        currentBorrowPosition: borrowed,
        targetLeverage,
        priceCollToDebt: swapQuote.priceAInB,
        flashLoanFee: new Decimal(flashLoanFee),
      });

      const calcsQuotePrice = adjustWithdrawLeverageCalcsCollFlash(
        adjustDepositPositionQuotePrice,
        adjustBorrowPositionQuotePrice,
        swapQuote.priceAInB,
        flashLoanFee,
        slippagePct
      );

      const swapInputAmountQuotePrice = toLamports(
        calcsQuotePrice.collTokenSwapIn,
        collReserve.getMintDecimals()
      ).ceil();

      return {
        swapInputs: {
          inputAmountLamports: swapInputAmountQuotePrice,
          minOutAmountLamports: toLamports(calcsQuotePrice.adjustBorrowPosition.abs(), debtReserve.stats.decimals),
          inputMint: collTokenMint,
          outputMint: debtTokenMint,
        },
        flashLoanInfo: klendIxs.flashLoanInfo,
        initialInputs: {
          calcs: calcsQuotePrice,
          swapQuote,
          currentSlot,
          obligation,
          klendAccounts: uniqueKlendAccounts,
          isDeposit,
        },
      };
    }
  }
}

export async function getAdjustLeverageIxs<QuoteResponse>({
  owner,
  kaminoMarket,
  debtReserveAddress,
  collReserveAddress,
  obligation,
  depositedLamports,
  borrowedLamports,
  referrer,
  currentSlot,
  targetLeverage,
  priceCollToDebt,
  priceDebtToColl,
  slippagePct,
  budgetAndPriorityFeeIxs,
  scopeRefreshIx,
  quoteBufferBps,
  quoter,
  swapper,
  useV2Ixs,
  withdrawSlotOffset,
  userSolBalanceLamports,
  flashBorrowType,
  logger,
}: AdjustLeverageProps<QuoteResponse>): Promise<Array<AdjustLeverageIxsResponse<QuoteResponse>>> {
  const log = logger ?? (() => {});
  const { swapInputs, initialInputs } = await getAdjustLeverageSwapInputs({
    owner,
    kaminoMarket,
    debtReserveAddress,
    collReserveAddress,
    obligation,
    depositedLamports,
    borrowedLamports,
    referrer,
    currentSlot,
    targetLeverage,
    priceCollToDebt,
    priceDebtToColl,
    slippagePct,
    budgetAndPriorityFeeIxs,
    scopeRefreshIx,
    quoteBufferBps,
    quoter,
    useV2Ixs,
    userSolBalanceLamports,
    flashBorrowType,
  });

  const effectiveFlashBorrowType = flashBorrowType || (initialInputs.isDeposit ? 'coll' : 'debt');

  log(initialInputs.isDeposit ? 'Increasing leverage' : 'Decreasing leverage', toJson(initialInputs.calcs));

  // leverage increased so we need to deposit and borrow more
  if (initialInputs.isDeposit) {
    const depositSwapper: SwapIxsProvider<QuoteResponse> = swapper;

    const swapsArray = await depositSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);

    const swapIxsArray = swapsArray.map((swap) => {
      return {
        preActionIxs: [] as Instruction[],
        swapIxs: swap.swapIxs,
        lookupTables: swap.lookupTables,
        quote: swap.quote,
      };
    });

    let increaseLeverageIxs: LeverageIxsOutput[];
    const increaseCalcs = initialInputs.calcs;
    if (effectiveFlashBorrowType !== 'debt') {
      // Coll flash path (default): flash borrow coll -> deposit -> borrow -> swap debt->coll -> flash repay coll
      assertAdjustIncreaseCollFlashCalcs(increaseCalcs);
      increaseLeverageIxs = await buildIncreaseLeverageIxsCollFlash(
        owner,
        kaminoMarket,
        collReserveAddress,
        debtReserveAddress,
        obligation,
        referrer,
        currentSlot,
        increaseCalcs,
        scopeRefreshIx,
        swapIxsArray,
        budgetAndPriorityFeeIxs,
        useV2Ixs
      );
    } else {
      // Debt flash path: flash borrow debt -> swap debt->coll -> deposit -> borrow -> flash repay debt
      assertAdjustIncreaseDebtFlashCalcs(increaseCalcs);
      increaseLeverageIxs = await buildIncreaseLeverageIxsDebtFlash(
        owner,
        kaminoMarket,
        collReserveAddress,
        debtReserveAddress,
        obligation,
        referrer,
        currentSlot,
        increaseCalcs,
        scopeRefreshIx,
        swapIxsArray,
        budgetAndPriorityFeeIxs,
        useV2Ixs
      );
    }

    return increaseLeverageIxs.map((ixs, index) => {
      return {
        ixs: ixs.instructions,
        flashLoanInfo: ixs.flashLoanInfo,
        lookupTables: swapsArray[index].lookupTables,
        swapInputs,
        initialInputs,
        quote: swapsArray[index].quote.quoteResponse,
      };
    });
  } else {
    const withdrawSwapper: SwapIxsProvider<QuoteResponse> = swapper;

    // 5. Get swap ixs
    const swapsArray = await withdrawSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);

    const swapIxsArray = swapsArray.map((swap) => {
      return {
        preActionIxs: [] as Instruction[],
        swapIxs: swap.swapIxs,
        lookupTables: swap.lookupTables,
        quote: swap.quote,
      };
    });

    let decreaseLeverageIxs: LeverageIxsOutput[];
    const decreaseCalcs = initialInputs.calcs;
    if (effectiveFlashBorrowType !== 'coll') {
      // Debt flash path (default): flash borrow debt -> repay -> withdraw -> swap coll->debt -> flash repay debt
      assertAdjustDecreaseDebtFlashCalcs(decreaseCalcs);
      decreaseLeverageIxs = await buildDecreaseLeverageIxsDebtFlash(
        owner,
        kaminoMarket,
        collReserveAddress,
        debtReserveAddress,
        obligation,
        referrer,
        currentSlot,
        decreaseCalcs,
        scopeRefreshIx,
        swapIxsArray,
        budgetAndPriorityFeeIxs,
        useV2Ixs,
        withdrawSlotOffset,
        userSolBalanceLamports
      );
    } else {
      // Coll flash path: flash borrow coll -> swap coll->debt -> repay -> withdraw -> flash repay coll
      assertAdjustDecreaseCollFlashCalcs(decreaseCalcs);
      decreaseLeverageIxs = await buildDecreaseLeverageIxsCollFlash(
        owner,
        kaminoMarket,
        collReserveAddress,
        debtReserveAddress,
        obligation,
        referrer,
        currentSlot,
        decreaseCalcs,
        scopeRefreshIx,
        swapIxsArray,
        budgetAndPriorityFeeIxs,
        useV2Ixs,
        withdrawSlotOffset,
        userSolBalanceLamports
      );
    }

    return decreaseLeverageIxs.map((ixs, index) => {
      return {
        ixs: ixs.instructions,
        flashLoanInfo: ixs.flashLoanInfo,
        lookupTables: swapsArray[index].lookupTables,
        swapInputs,
        initialInputs,
        quote: swapsArray[index].quote.quoteResponse,
      };
    });
  }
}

function buildFlashLeverageOutputs<QuoteResponse>(
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  ixsBeforeSwap: Instruction[],
  ixsAfterSwap: Instruction[],
  budgetIxs: Instruction[],
  flashBorrowReserve: KaminoReserve
): LeverageIxsOutput[] {
  return swapQuoteIxsArray.map((swapQuoteIxs) => ({
    flashLoanInfo: {
      flashBorrowReserve: flashBorrowReserve.address,
      flashLoanFee: flashBorrowReserve.getFlashLoanFee(),
    },
    instructions: [...ixsBeforeSwap, ...removeBudgetIxs(swapQuoteIxs.swapIxs), ...ixsAfterSwap, ...budgetIxs],
  }));
}

/**
 * Deposit and borrow tokens if leverage increased
 */
async function buildIncreaseLeverageIxsCollFlash<QuoteResponse>(
  owner: TransactionSigner,
  kaminoMarket: KaminoMarket,
  collReserveAddress: Address,
  debtReserveAddress: Address,
  obligation: KaminoObligation,
  referrer: Option<Address>,
  currentSlot: Slot,
  calcs: AdjustLeverageCalcsResult,
  scopeRefreshIx: Instruction[],
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  useV2Ixs: boolean
): Promise<LeverageIxsOutput[]> {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const debtTokenMint = debtReserve.getLiquidityMint();
  const collTokenAta = await getAssociatedTokenAddress(
    collTokenMint,
    owner.address,
    collReserve.getLiquidityTokenProgram()
  );

  // 1. Create atas & budget txns
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  // 2. Create borrow flash loan instruction
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + scopeRefreshIx.length, // TODO: how about user metadata ixs
    userTransferAuthority: owner,
    lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(),
    lendingMarketAddress: kaminoMarket.getAddress(),
    reserve: collReserve!,
    amountLamports: toLamports(calcs.adjustDepositPosition, collReserve!.stats.decimals),
    destinationAta: collTokenAta,
    // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: kaminoMarket.programId,
  });

  const depositAction = await KaminoAction.buildDepositTxns({
    kaminoMarket,
    amount: toLamports(calcs.adjustDepositPosition, collReserve.stats.decimals).floor().toString(),
    reserveAddress: collReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
    currentSlot,
  });

  // 4. Borrow tokens in borrow token reserve that will be swapped to repay flash loan
  const borrowAction = await KaminoAction.buildBorrowTxns({
    kaminoMarket,
    amount: toLamports(calcs.borrowAmount, debtReserve.stats.decimals).ceil().toString(),
    reserveAddress: debtReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit)
    referrer,
    currentSlot,
  });

  return buildFlashLeverageOutputs(
    swapQuoteIxsArray,
    [
      ...scopeRefreshIx,
      ...createAtasIxs,
      ...[flashBorrowIx],
      ...KaminoAction.actionToIxs(depositAction),
      ...KaminoAction.actionToIxs(borrowAction),
    ],
    [flashRepayIx],
    budgetIxs,
    collReserve
  );
}

/**
 * Increase leverage with flash borrow DEBT token.
 * Order: scopeRefresh → createAtas → flashBorrow(DEBT) → swap(debt→coll) → deposit → borrow → flashRepay(DEBT) → budget
 */
async function buildIncreaseLeverageIxsDebtFlash<QuoteResponse>(
  owner: TransactionSigner,
  kaminoMarket: KaminoMarket,
  collReserveAddress: Address,
  debtReserveAddress: Address,
  obligation: KaminoObligation,
  referrer: Option<Address>,
  currentSlot: Slot,
  calcs: AdjustDepositDebtFlashCalcsResult,
  scopeRefreshIx: Instruction[],
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  useV2Ixs: boolean
): Promise<LeverageIxsOutput[]> {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const [debtTokenAta] = await findAssociatedTokenPda({
    owner: owner.address,
    mint: debtTokenMint,
    tokenProgram: debtReserve.getLiquidityTokenProgram(),
  });

  // 1. Create atas & budget txns
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  // 2. Flash borrow DEBT
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(),
    lendingMarketAddress: kaminoMarket.getAddress(),
    reserve: debtReserve,
    amountLamports: toLamports(calcs.flashBorrowInDebtToken, debtReserve.stats.decimals),
    destinationAta: debtTokenAta,
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: kaminoMarket.programId,
  });

  // 3. Deposit coll
  const depositAction = await KaminoAction.buildDepositTxns({
    kaminoMarket,
    amount: toLamports(calcs.adjustDepositPosition, collReserve.stats.decimals).floor().toString(),
    reserveAddress: collReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
    currentSlot,
  });

  // 4. Borrow debt to repay flash
  const borrowAction = await KaminoAction.buildBorrowTxns({
    kaminoMarket,
    amount: toLamports(calcs.debtTokenToBorrow, debtReserve.stats.decimals).ceil().toString(),
    reserveAddress: debtReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
    currentSlot,
  });

  return buildFlashLeverageOutputs(
    swapQuoteIxsArray,
    [...scopeRefreshIx, ...createAtasIxs, ...[flashBorrowIx]],
    [...KaminoAction.actionToIxs(depositAction), ...KaminoAction.actionToIxs(borrowAction), ...[flashRepayIx]],
    budgetIxs,
    debtReserve
  );
}

/**
 * Withdraw and repay tokens if leverage decreased
 */
async function buildDecreaseLeverageIxsDebtFlash<QuoteResponse>(
  owner: TransactionSigner,
  kaminoMarket: KaminoMarket,
  collReserveAddress: Address,
  debtReserveAddress: Address,
  obligation: KaminoObligation,
  referrer: Option<Address>,
  currentSlot: Slot,
  calcs: AdjustLeverageCalcsResult,
  scopeRefreshIx: Instruction[],
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  useV2Ixs: boolean,
  withdrawSlotOffset: number = WITHDRAW_SLOT_OFFSET,
  userSolBalanceLamports: number
): Promise<LeverageIxsOutput[]> {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const [debtTokenAta] = await findAssociatedTokenPda({
    owner: owner.address,
    mint: debtTokenMint,
    tokenProgram: debtReserve.getLiquidityTokenProgram(),
  });

  // 1. Create atas & budget txns
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  // TODO: Mihai/Marius check if we can improve this logic and not convert any SOL
  // This is here so that we have enough wsol to repay in case the kAB swapped to sol after estimates is not enough
  const closeWsolAtaIxs: Instruction[] = [];
  const fillWsolAtaIxs: Instruction[] = [];
  if (debtTokenMint === WRAPPED_SOL_MINT) {
    const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address);

    closeWsolAtaIxs.push(
      getCloseAccountInstruction(
        {
          owner,
          account: wsolAta,
          destination: owner.address,
        },
        { programAddress: TOKEN_PROGRAM_ADDRESS }
      )
    );

    const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2;
    const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1;
    fillWsolAtaIxs.push(
      ...getTransferWsolIxs(
        owner,
        wsolAta,
        lamports(BigInt(toLamports(balanceToWrap, debtReserve!.stats.decimals).ceil().toString()))
      )
    );
  }

  // 3. Flash borrow & repay amount to repay (debt)
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(),
    lendingMarketAddress: kaminoMarket.getAddress(),
    reserve: debtReserve!,
    amountLamports: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve!.stats.decimals),
    destinationAta: debtTokenAta,
    // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: kaminoMarket.programId,
  });

  // 4. Actually do the repay of the flash borrowed amounts
  const repayAction = await KaminoAction.buildRepayTxns({
    kaminoMarket,
    amount: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve!.stats.decimals).floor().toString(),
    reserveAddress: debtReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    currentSlot,
    payer: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit)
    referrer,
  });

  const withdrawSlot = currentSlot - BigInt(withdrawSlotOffset);
  // 6. Withdraw collateral (a little bit more to be able to pay for the slippage on swap)
  const withdrawAction = await KaminoAction.buildWithdrawTxns({
    kaminoMarket,
    amount: toLamports(calcs.withdrawAmountWithSlippageAndFlashLoanFee, collReserve!.stats.decimals).ceil().toString(),
    reserveAddress: collReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit)
    referrer,
    currentSlot: withdrawSlot,
  });

  return swapQuoteIxsArray.map((swapQuoteIxs) => {
    const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);

    const ixs = [
      ...scopeRefreshIx,
      ...createAtasIxs,
      ...fillWsolAtaIxs,
      ...[flashBorrowIx],
      ...KaminoAction.actionToIxs(repayAction),
      ...KaminoAction.actionToIxs(withdrawAction),
      ...swapInstructions,
      ...[flashRepayIx],
      ...closeWsolAtaIxs,
      ...budgetIxs,
    ];

    const res: LeverageIxsOutput = {
      flashLoanInfo: {
        flashBorrowReserve: debtReserve!.address,
        flashLoanFee: debtReserve!.getFlashLoanFee(),
      },
      instructions: ixs,
    };

    return res;
  });
}

/**
 * Decrease leverage with flash borrow COLLATERAL token.
 * Order: scopeRefresh → createAtas → fillWsol → flashBorrow(COLL) → swap(coll→debt) → repay → withdraw → flashRepay(COLL) → closeWsol → budget
 */
async function buildDecreaseLeverageIxsCollFlash<QuoteResponse>(
  owner: TransactionSigner,
  kaminoMarket: KaminoMarket,
  collReserveAddress: Address,
  debtReserveAddress: Address,
  obligation: KaminoObligation,
  referrer: Option<Address>,
  currentSlot: Slot,
  calcs: AdjustWithdrawCollFlashCalcsResult,
  scopeRefreshIx: Instruction[],
  swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
  budgetAndPriorityFeeIxs: Instruction[] | undefined,
  useV2Ixs: boolean,
  withdrawSlotOffset: number = WITHDRAW_SLOT_OFFSET,
  userSolBalanceLamports: number
): Promise<LeverageIxsOutput[]> {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  const [collTokenAta] = await findAssociatedTokenPda({
    owner: owner.address,
    mint: collTokenMint,
    tokenProgram: collReserve.getLiquidityTokenProgram(),
  });

  // 1. Create atas & budget txns
  const { budgetIxs, createAtasIxs } = await getSetupIxs(
    owner,
    collTokenMint,
    collReserve,
    debtTokenMint,
    debtReserve,
    budgetAndPriorityFeeIxs
  );

  const closeWsolAtaIxs: Instruction[] = [];
  const fillWsolAtaIxs: Instruction[] = [];
  if (collTokenMint === WRAPPED_SOL_MINT) {
    const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address);

    closeWsolAtaIxs.push(
      getCloseAccountInstruction(
        {
          owner,
          account: wsolAta,
          destination: owner.address,
        },
        { programAddress: TOKEN_PROGRAM_ADDRESS }
      )
    );

    const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2;
    const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1;
    fillWsolAtaIxs.push(
      ...getTransferWsolIxs(
        owner,
        wsolAta,
        lamports(BigInt(toLamports(balanceToWrap, collReserve.stats.decimals).ceil().toString()))
      )
    );
  }

  // 2. Flash borrow COLL
  const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
    borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + scopeRefreshIx.length,
    userTransferAuthority: owner,
    lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(),
    lendingMarketAddress: kaminoMarket.getAddress(),
    reserve: collReserve,
    amountLamports: toLamports(calcs.flashBorrowInCollToken, collReserve.stats.decimals),
    destinationAta: collTokenAta,
    referrerAccount: none(),
    referrerTokenState: none(),
    programId: kaminoMarket.programId,
  });

  // 3. Repay debt
  const repayAction = await KaminoAction.buildRepayTxns({
    kaminoMarket,
    amount: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve.stats.decimals).floor().toString(),
    reserveAddress: debtReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    currentSlot,
    payer: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
  });

  // 4. Withdraw coll
  const withdrawSlot = currentSlot - BigInt(withdrawSlotOffset);
  const withdrawAction = await KaminoAction.buildWithdrawTxns({
    kaminoMarket,
    amount: toLamports(calcs.depositTokenWithdrawAmount, collReserve.stats.decimals).ceil().toString(),
    reserveAddress: collReserve.address,
    owner,
    obligation,
    useV2Ixs,
    scopeRefreshConfig: undefined,
    extraComputeBudget: 0,
    includeAtaIxs: false,
    requestElevationGroup: false,
    initUserMetadata: { skipInitialization: true, skipLutCreation: true },
    referrer,
    currentSlot: withdrawSlot,
  });

  return swapQuoteIxsArray.map((swapQuoteIxs) => {
    const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);

    // Key difference: swap BEFORE repay+withdraw
    const ixs = [
      ...scopeRefreshIx,
      ...createAtasIxs,
      ...fillWsolAtaIxs,
      ...[flashBorrowIx],
      ...swapInstructions,
      ...KaminoAction.actionToIxs(repayAction),
      ...KaminoAction.actionToIxs(withdrawAction),
      ...[flashRepayIx],
      ...closeWsolAtaIxs,
      ...budgetIxs,
    ];

    const res: LeverageIxsOutput = {
      flashLoanInfo: {
        flashBorrowReserve: collReserve.address,
        flashLoanFee: collReserve.getFlashLoanFee(),
      },
      instructions: ixs,
    };

    return res;
  });
}

export const getSetupIxs = async (
  owner: TransactionSigner,
  collTokenMint: Address,
  collReserve: KaminoReserve,
  debtTokenMint: Address,
  debtReserve: KaminoReserve,
  budgetAndPriorityFeeIxs: Instruction[] | undefined
) => {
  const budgetIxs = budgetAndPriorityFeeIxs || getComputeBudgetAndPriorityFeeIxs(3000000);

  const mintsWithTokenPrograms = getTokenMintsWithTokenPrograms(collTokenMint, collReserve, debtTokenMint, debtReserve);

  const createAtasIxs = (await createAtasIdempotent(owner, mintsWithTokenPrograms)).map((x) => x.createAtaIx);

  return {
    budgetIxs,
    createAtasIxs,
  };
};

export const getScopeRefreshIxForObligationAndReserves = async (
  market: KaminoMarket,
  collReserve: KaminoReserve,
  debtReserve: KaminoReserve,
  obligation: KaminoObligation | ObligationType | undefined,
  scopeRefreshConfig: ScopePriceRefreshConfig | undefined
): Promise<Instruction[]> => {
  const allReserves =
    obligation && isKaminoObligation(obligation)
      ? [
          ...new Set<Address>([
            ...obligation.getDeposits().map((x) => x.reserveAddress),
            ...obligation.getBorrows().map((x) => x.reserveAddress),
            collReserve.address,
            debtReserve.address,
          ]),
        ]
      : [...new Set<Address>([collReserve.address, debtReserve.address])];

  const scopeRefreshIxs: Instruction[] = [];
  const scopeTokensMap = getTokenIdsForScopeRefresh(market, allReserves);

  if (scopeTokensMap.size > 0 && scopeRefreshConfig) {
    for (const [configPubkey, config] of scopeRefreshConfig.scopeConfigurations) {
      const tokenIds = scopeTokensMap.get(config.oraclePrices);
      if (tokenIds && tokenIds.length > 0) {
        const refreshIx = await scopeRefreshConfig.scope.refreshPriceListIx({ config: configPubkey }, tokenIds);
        if (refreshIx) {
          scopeRefreshIxs.push(refreshIx);
        }
      }
    }
  }

  return scopeRefreshIxs;
};

const checkObligationType = (
  obligationTypeTag: ObligationTypeTag,
  collReserveAddress: Address,
  debtReserveAddress: Address,
  kaminoMarket: KaminoMarket
) => {
  const collReserve = kaminoMarket.getExistingReserveByAddress(collReserveAddress);
  const debtReserve = kaminoMarket.getExistingReserveByAddress(debtReserveAddress);
  const collTokenMint = collReserve.getLiquidityMint();
  const debtTokenMint = debtReserve.getLiquidityMint();
  let obligationType: ObligationType;
  if (obligationTypeTag === ObligationTypeTag.Multiply) {
    // multiply
    obligationType = new MultiplyObligation(collTokenMint, debtTokenMint, kaminoMarket.programId);
  } else if (obligationTypeTag === ObligationTypeTag.Leverage) {
    // leverage
    obligationType = new LeverageObligation(collTokenMint, debtTokenMint, kaminoMarket.programId);
  } else if (obligationTypeTag === ObligationTypeTag.MultiplyFixedRate) {
    // multiply fixed rate
    obligationType = new MultiplyObligationFixedRate(collReserveAddress, debtReserveAddress, kaminoMarket.programId);
  } else if (obligationTypeTag === ObligationTypeTag.LeverageFixedRate) {
    // leverage fixed rate
    obligationType = new LeverageObligationFixedRate(collReserveAddress, debtReserveAddress, kaminoMarket.programId);
  } else {
    throw Error('Obligation type tag not supported for leverage, please use 1 - multiply or 3 - leverage');
  }

  return obligationType;
};

type MintWithTokenProgram = {
  mint: Address;
  tokenProgram: Address;
};

const getTokenMintsWithTokenPrograms = (
  collTokenMint: Address,
  collReserve: KaminoReserve,
  debtTokenMint: Address,
  debtReserve: KaminoReserve
): Array<MintWithTokenProgram> => {
  return [
    {
      mint: collTokenMint,
      tokenProgram: collReserve.getLiquidityTokenProgram(),
    },
    {
      mint: debtTokenMint,
      tokenProgram: debtReserve.getLiquidityTokenProgram(),
    },
    {
      mint: collReserve.getCTokenMint(),
      tokenProgram: TOKEN_PROGRAM_ADDRESS,
    },
  ];
};
