import {
  Address,
  GetContractReturnType,
  Hash,
  Hex,
  Log,
  decodeEventLog,
  encodeEventTopics,
  getAddress,
  getContract,
  zeroAddress,
} from 'viem'

import {
  BaseClientMixin,
  BaseGasEstimatesMixin,
  BaseTransactions,
} from './base'
import {
  TransactionType,
  getSwapperFactoryAddress,
  SWAPPER_CHAIN_IDS,
  getUniV3SwapAddress,
  PERMISSIONLESS_SWAPPER_CHAIN_IDS,
  UNIVERSAL_SWAP_ADDRESS,
} from '../constants'
import { swapperFactoryAbi } from '../constants/abi/swapperFactory'
import { uniV3SwapAbi } from '../constants/abi/uniV3Swap'
import { swapperAbi } from '../constants/abi/swapper'
import {
  InvalidArgumentError,
  InvalidAuthError,
  TransactionFailedError,
} from '../errors'
import { applyMixins } from './mixin'
import type {
  CallData,
  ContractQuoteParams,
  ContractSwapperExactInputParams,
  CreateSwapperConfig,
  ReadContractArgs,
  SplitsClientConfig,
  SplitsPublicClient,
  SwapperExecCallsConfig,
  SwapperPauseConfig,
  SwapperSetBeneficiaryConfig,
  SwapperSetDefaultScaledOfferFactorConfig,
  SwapperSetOracleConfig,
  SwapperSetScaledOfferFactorOverridesConfig,
  SwapperSetTokenToBeneficiaryConfig,
  TransactionConfig,
  TransactionFormat,
  UniV3FlashSwapConfig,
  UniversalSwapConfig,
} from '../types'
import {
  getFormattedOracleParams,
  getFormattedScaledOfferFactor,
  getFormattedScaledOfferFactorOverrides,
} from '../utils'
import {
  validateAddress,
  validateCalls,
  validateOracleParams,
  validateScaledOfferFactor,
  validateScaledOfferFactorOverrides,
  validateUniV3SwapInputAssets,
} from '../utils/validation'
import { universalSwapAbi } from '../constants/abi/universalSwap'

type SwapperAbi = typeof swapperAbi
type UniV3SwapAbi = typeof uniV3SwapAbi

class SwapperTransactions extends BaseTransactions {
  constructor(transactionClientArgs: SplitsClientConfig & TransactionConfig) {
    super({
      supportedChainIds: SWAPPER_CHAIN_IDS,
      ...transactionClientArgs,
    })
  }

  protected async _createSwapperTransaction({
    owner,
    paused = false,
    beneficiary,
    tokenToBeneficiary,
    oracleParams,
    defaultScaledOfferFactorPercent,
    scaledOfferFactorOverrides,
    transactionOverrides = {},
  }: CreateSwapperConfig): Promise<TransactionFormat> {
    validateAddress(owner)
    validateAddress(beneficiary)
    validateAddress(tokenToBeneficiary)
    validateOracleParams(oracleParams)
    validateScaledOfferFactor(defaultScaledOfferFactorPercent)
    validateScaledOfferFactorOverrides(scaledOfferFactorOverrides)
    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const formattedOracleParams = getFormattedOracleParams(oracleParams)
    const formattedDefaultScaledOfferFactor = getFormattedScaledOfferFactor(
      defaultScaledOfferFactorPercent,
    )
    const formattedScaledOfferFactorOverrides =
      getFormattedScaledOfferFactorOverrides(scaledOfferFactorOverrides)

    const result = await this._executeContractFunction({
      contractAddress: getSwapperFactoryAddress(),
      contractAbi: swapperFactoryAbi,
      functionName: 'createSwapper',
      functionArgs: [
        [
          owner,
          paused,
          beneficiary,
          tokenToBeneficiary,
          formattedOracleParams,
          formattedDefaultScaledOfferFactor,
          formattedScaledOfferFactorOverrides,
        ],
      ],
      transactionOverrides,
    })

    return result
  }

  protected async _uniV3FlashSwapTransaction({
    swapperAddress,
    excessRecipient,
    inputAssets,
    transactionTimeLimit = 300,
    chainId,
    transactionOverrides = {},
  }: UniV3FlashSwapConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateUniV3SwapInputAssets(inputAssets)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const functionChainId = this._getFunctionChainId(chainId)

    const excessRecipientAddress = excessRecipient
      ? excessRecipient
      : this._walletClient?.account
        ? this._walletClient.account.address
        : zeroAddress
    validateAddress(excessRecipientAddress)

    this._requireDataClient()

    // TO DO: handle bad swapper id/no metadata found
    const { tokenToBeneficiary } = await this._dataClient!.getSwapperMetadata({
      chainId: functionChainId,
      swapperAddress,
    })

    if (!PERMISSIONLESS_SWAPPER_CHAIN_IDS.includes(functionChainId)) {
      if (inputAssets.length > 1) {
        throw new InvalidArgumentError(
          `Chain id: ${functionChainId} - swappers only support one input asset, swapper owners can use the execCalls function to execute multiple swaps.`,
        )
      }

      if (inputAssets[0]?.token !== tokenToBeneficiary.address) {
        throw new InvalidArgumentError(
          `Chain id: ${functionChainId} - swappers only support the tokenToBeneficiary as the input asset, swapper owners can use the execCalls function to execute this swap.`,
        )
      }
    }

    const swapRecipient = getUniV3SwapAddress(functionChainId)
    const deadlineTime = Math.floor(Date.now() / 1000) + transactionTimeLimit

    const quoteParams: ContractQuoteParams[] = []
    const exactInputParams: ContractSwapperExactInputParams[] = []
    inputAssets.map((inputAsset) => {
      quoteParams.push([
        [inputAsset.token, tokenToBeneficiary.address],
        inputAsset.amountIn,
        '0x',
      ])
      if (inputAsset.encodedPath) {
        exactInputParams.push([
          inputAsset.encodedPath,
          swapRecipient,
          deadlineTime,
          inputAsset.amountIn,
          inputAsset.amountOutMin,
        ])
      }
    })

    const flashParams = [
      quoteParams,
      [exactInputParams, excessRecipientAddress],
    ]

    const result = await this._executeContractFunction({
      contractAddress: getUniV3SwapAddress(functionChainId),
      contractAbi: uniV3SwapAbi,
      functionName: 'initFlash',
      functionArgs: [swapperAddress, flashParams],
      transactionOverrides,
    })

    return result
  }

  protected async _universalSwapTransaction({
    swapperAddress,
    excessRecipient,
    calls,
    inputAssets,
    chainId,
    transactionOverrides = {},
  }: UniversalSwapConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateCalls(calls)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const functionChainId = this._getFunctionChainId(chainId)

    const excessRecipientAddress = excessRecipient
      ? excessRecipient
      : this._walletClient?.account
        ? this._walletClient.account.address
        : zeroAddress
    validateAddress(excessRecipientAddress)

    this._requirePublicClient(functionChainId)

    const tokenToBeneficiary = await this._publicClient!.readContract({
      address: getAddress(swapperAddress),
      abi: swapperAbi,
      functionName: 'tokenToBeneficiary',
    })

    const quoteParams: ContractQuoteParams[] = []
    inputAssets.map((inputAsset) => {
      quoteParams.push([
        [inputAsset.token, tokenToBeneficiary],
        inputAsset.amountIn,
        '0x',
      ])
    })

    const flashParams = [quoteParams, [calls, excessRecipientAddress]]

    const result = await this._executeContractFunction({
      contractAddress: UNIVERSAL_SWAP_ADDRESS,
      contractAbi: universalSwapAbi,
      functionName: 'initFlash',
      functionArgs: [swapperAddress, flashParams],
      transactionOverrides,
    })

    return result
  }

  protected async _setBeneficiaryTransaction({
    swapperAddress,
    beneficiary,
    transactionOverrides = {},
  }: SwapperSetBeneficiaryConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateAddress(beneficiary)
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'setBeneficiary',
      functionArgs: [beneficiary],
      transactionOverrides,
    })

    return result
  }

  protected async _setTokenToBeneficiaryTransaction({
    swapperAddress,
    tokenToBeneficiary,
    transactionOverrides = {},
  }: SwapperSetTokenToBeneficiaryConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateAddress(tokenToBeneficiary)
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'setTokenToBeneficiary',
      functionArgs: [tokenToBeneficiary],
      transactionOverrides,
    })

    return result
  }

  protected async _setOracleTransaction({
    swapperAddress,
    oracle,
    transactionOverrides = {},
  }: SwapperSetOracleConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateAddress(oracle)
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'setOracle',
      functionArgs: [oracle],
      transactionOverrides,
    })

    return result
  }

  protected async _setDefaultScaledOfferFactorTransaction({
    swapperAddress,
    defaultScaledOfferFactorPercent,
    transactionOverrides = {},
  }: SwapperSetDefaultScaledOfferFactorConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateScaledOfferFactor(defaultScaledOfferFactorPercent)
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const formattedDefaultScaledOfferFactor = getFormattedScaledOfferFactor(
      defaultScaledOfferFactorPercent,
    )

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'setDefaultScaledOfferFactor',
      functionArgs: [formattedDefaultScaledOfferFactor],
      transactionOverrides,
    })

    return result
  }

  protected async _setScaledOfferFactorOverridesTransaction({
    swapperAddress,
    scaledOfferFactorOverrides,
    transactionOverrides = {},
  }: SwapperSetScaledOfferFactorOverridesConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    validateScaledOfferFactorOverrides(scaledOfferFactorOverrides)
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const formattedScaledOfferFactorOverrides =
      getFormattedScaledOfferFactorOverrides(scaledOfferFactorOverrides)

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'setPairScaledOfferFactors',
      functionArgs: [formattedScaledOfferFactorOverrides],
      transactionOverrides,
    })

    return result
  }

  protected async _execCallsTransaction({
    swapperAddress,
    calls,
    transactionOverrides = {},
  }: SwapperExecCallsConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    calls.map((callData) => validateAddress(callData.to))
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const formattedCalls = calls.map((callData) => {
      return [callData.to, callData.value, callData.data]
    })

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'execCalls',
      functionArgs: [formattedCalls],
      transactionOverrides,
    })

    return result
  }

  protected async _setPausedTransaction({
    swapperAddress,
    paused,
    transactionOverrides = {},
  }: SwapperPauseConfig): Promise<TransactionFormat> {
    validateAddress(swapperAddress)
    if (this._shouldRequireWalletClient) {
      this._requireWalletClient()
      await this._requireOwner(swapperAddress)
    }

    const result = await this._executeContractFunction({
      contractAddress: getAddress(swapperAddress),
      contractAbi: swapperAbi,
      functionName: 'setPaused',
      functionArgs: [paused],
      transactionOverrides,
    })

    return result
  }

  private async _requireOwner(swapperAddress: string) {
    this._requireWalletClient()

    const swapperContract = this._getSwapperContract(
      swapperAddress,
      this._walletClient!.chain!.id,
    )
    const owner = await swapperContract.read.owner()

    const walletAddress = this._walletClient!.account?.address

    if (owner !== walletAddress)
      throw new InvalidAuthError(
        `Action only available to the swapper owner. Swapper address: ${swapperAddress}, owner: ${owner}, wallet address: ${walletAddress}`,
      )
  }

  protected _getUniV3SwapContract(
    chainId: number,
  ): GetContractReturnType<UniV3SwapAbi, SplitsPublicClient> {
    const publicClient = this._getPublicClient(chainId)
    return getContract({
      address: getUniV3SwapAddress(chainId),
      abi: uniV3SwapAbi,
      client: publicClient,
    })
  }

  protected _getSwapperContract(
    swapper: string,
    chainId: number,
  ): GetContractReturnType<SwapperAbi, SplitsPublicClient> {
    const publicClient = this._getPublicClient(chainId)
    return getContract({
      address: getAddress(swapper),
      abi: swapperAbi,
      client: publicClient,
    })
  }
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class SwapperClient extends SwapperTransactions {
  readonly eventTopics: { [key: string]: Hex[] }
  readonly callData: SwapperCallData
  readonly estimateGas: SwapperGasEstimates

  constructor(clientArgs: SplitsClientConfig) {
    super({
      transactionType: TransactionType.Transaction,
      ...clientArgs,
    })
    this.eventTopics = {
      createSwapper: [
        encodeEventTopics({
          abi: swapperFactoryAbi,
          eventName: 'CreateSwapper',
        })[0],
      ],
      swapperFlash: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'Flash',
        })[0],
      ],
      execCalls: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'ExecCalls',
        })[0],
      ],
      setPaused: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'SetPaused',
        })[0],
      ],
      setBeneficiary: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'SetBeneficiary',
        })[0],
      ],
      setTokenToBeneficiary: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'SetTokenToBeneficiary',
        })[0],
      ],
      setOracle: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'SetOracle',
        })[0],
      ],
      setDefaultScaledOfferFactor: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'SetDefaultScaledOfferFactor',
        })[0],
      ],
      setScaledOfferFactorOverrides: [
        encodeEventTopics({
          abi: swapperAbi,
          eventName: 'SetPairScaledOfferFactors',
        })[0],
      ],
    }

    this.callData = new SwapperCallData(clientArgs)
    this.estimateGas = new SwapperGasEstimates(clientArgs)
  }

  // Write actions
  async _submitCreateSwapperTransaction(
    createSwapperArgs: CreateSwapperConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._createSwapperTransaction(createSwapperArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async createSwapper(createSwapperArgs: CreateSwapperConfig): Promise<{
    swapperAddress: Address
    event: Log
  }> {
    const { txHash } =
      await this._submitCreateSwapperTransaction(createSwapperArgs)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.createSwapper,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event) {
      const log = decodeEventLog({
        abi: swapperFactoryAbi,
        data: event.data,
        topics: event.topics,
      })
      return {
        swapperAddress: log.args.swapper,
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async _submitUniV3FlashSwapTransaction(
    flashArgs: UniV3FlashSwapConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._uniV3FlashSwapTransaction(flashArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async uniV3FlashSwap(flashArgs: UniV3FlashSwapConfig): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitUniV3FlashSwapTransaction(flashArgs)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.swapperFlash,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitUniversalSwapTransaction(
    flashArgs: UniversalSwapConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._universalSwapTransaction(flashArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async universalSwap(flashArgs: UniversalSwapConfig): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitUniversalSwapTransaction(flashArgs)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.swapperFlash,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitExecCallsTransaction(callArgs: SwapperExecCallsConfig): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._execCallsTransaction(callArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async execCalls(callArgs: SwapperExecCallsConfig): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitExecCallsTransaction(callArgs)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.execCalls,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitSetPausedTransaction(pauseArgs: SwapperPauseConfig): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._setPausedTransaction(pauseArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async setPaused(pauseArgs: SwapperPauseConfig): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitSetPausedTransaction(pauseArgs)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.setPaused,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitSetBeneficiaryTransaction(
    args: SwapperSetBeneficiaryConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._setBeneficiaryTransaction(args)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async setBeneficiary(args: SwapperSetBeneficiaryConfig): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitSetBeneficiaryTransaction(args)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.setBeneficiary,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitSetTokenToBeneficiaryTransaction(
    args: SwapperSetTokenToBeneficiaryConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._setTokenToBeneficiaryTransaction(args)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async setTokenToBeneficiary(
    args: SwapperSetTokenToBeneficiaryConfig,
  ): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitSetTokenToBeneficiaryTransaction(args)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.setTokenToBeneficiary,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitSetOracleTransaction(args: SwapperSetOracleConfig): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._setOracleTransaction(args)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async setOracle(args: SwapperSetOracleConfig): Promise<{
    event: Log
  }> {
    const { txHash } = await this._submitSetOracleTransaction(args)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.setOracle,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitSetDefaultScaledOfferFactorTransaction(
    args: SwapperSetDefaultScaledOfferFactorConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._setDefaultScaledOfferFactorTransaction(args)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async setDefaultScaledOfferFactor(
    args: SwapperSetDefaultScaledOfferFactorConfig,
  ): Promise<{
    event: Log
  }> {
    const { txHash } =
      await this._submitSetDefaultScaledOfferFactorTransaction(args)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.setDefaultScaledOfferFactor,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  async _submitSetScaledOfferFactorOverridesTransaction(
    args: SwapperSetScaledOfferFactorOverridesConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._setScaledOfferFactorOverridesTransaction(args)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async setScaledOfferFactorOverrides(
    args: SwapperSetScaledOfferFactorOverridesConfig,
  ): Promise<{
    event: Log
  }> {
    const { txHash } =
      await this._submitSetScaledOfferFactorOverridesTransaction(args)
    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.setScaledOfferFactorOverrides,
    })
    const event = events.length > 0 ? events[0] : undefined
    if (event)
      return {
        event,
      }

    throw new TransactionFailedError()
  }

  // Read actions
  async getBeneficiary({
    swapperAddress,
    chainId,
  }: ReadContractArgs & {
    swapperAddress: string
  }): Promise<{
    beneficiary: Address
  }> {
    validateAddress(swapperAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    const swapperContract = this._getSwapperContract(
      swapperAddress,
      functionChainId,
    )
    const beneficiary = await swapperContract.read.beneficiary()

    return {
      beneficiary,
    }
  }

  async getTokenToBeneficiary({
    swapperAddress,
    chainId,
  }: ReadContractArgs & {
    swapperAddress: string
  }): Promise<{
    tokenToBeneficiary: Address
  }> {
    validateAddress(swapperAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    const swapperContract = this._getSwapperContract(
      swapperAddress,
      functionChainId,
    )
    const tokenToBeneficiary = await swapperContract.read.tokenToBeneficiary()

    return {
      tokenToBeneficiary,
    }
  }

  async getOracle({
    swapperAddress,
    chainId,
  }: ReadContractArgs & { swapperAddress: string }): Promise<{
    oracle: Address
  }> {
    validateAddress(swapperAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    const swapperContract = this._getSwapperContract(
      swapperAddress,
      functionChainId,
    )
    const oracle = await swapperContract.read.oracle()

    return {
      oracle,
    }
  }

  async getDefaultScaledOfferFactor({
    swapperAddress,
    chainId,
  }: ReadContractArgs & {
    swapperAddress: string
  }): Promise<{
    defaultScaledOfferFactor: number
  }> {
    validateAddress(swapperAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    const swapperContract = this._getSwapperContract(
      swapperAddress,
      functionChainId,
    )
    const defaultScaledOfferFactor =
      await swapperContract.read.defaultScaledOfferFactor()

    return {
      defaultScaledOfferFactor,
    }
  }

  async getScaledOfferFactorOverrides({
    swapperAddress,
    quotePairs,
    chainId,
  }: ReadContractArgs & {
    swapperAddress: string
    quotePairs: {
      base: string
      quote: string
    }[]
  }): Promise<{
    scaledOfferFactorOverrides: number[]
  }> {
    validateAddress(swapperAddress)
    quotePairs.map((quotePair) => {
      validateAddress(quotePair.base)
      validateAddress(quotePair.quote)
    })

    const formattedQuotePairs = quotePairs.map((quotePair) => {
      return {
        base: getAddress(quotePair.base),
        quote: getAddress(quotePair.quote),
      }
    })

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    const swapperContract = this._getSwapperContract(
      swapperAddress,
      functionChainId,
    )
    const scaledOfferFactorOverrides =
      await swapperContract.read.getPairScaledOfferFactors([
        formattedQuotePairs,
      ])

    return {
      scaledOfferFactorOverrides: scaledOfferFactorOverrides.slice(),
    }
  }
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export interface SwapperClient extends BaseClientMixin {}
applyMixins(SwapperClient, [BaseClientMixin])

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class SwapperGasEstimates extends SwapperTransactions {
  constructor(clientArgs: SplitsClientConfig) {
    super({
      transactionType: TransactionType.GasEstimate,
      ...clientArgs,
    })
  }

  async createSwapper(createSwapperArgs: CreateSwapperConfig): Promise<bigint> {
    const gasEstimate = await this._createSwapperTransaction(createSwapperArgs)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async uniV3FlashSwap(flashArgs: UniV3FlashSwapConfig): Promise<bigint> {
    const gasEstimate = await this._uniV3FlashSwapTransaction(flashArgs)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async universalSwap(flashArgs: UniversalSwapConfig): Promise<bigint> {
    const gasEstimate = await this._universalSwapTransaction(flashArgs)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async execCalls(callArgs: SwapperExecCallsConfig): Promise<bigint> {
    const gasEstimate = await this._execCallsTransaction(callArgs)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setPaused(args: SwapperPauseConfig): Promise<bigint> {
    const gasEstimate = await this._setPausedTransaction(args)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setBeneficiary(args: SwapperSetBeneficiaryConfig): Promise<bigint> {
    const gasEstimate = await this._setBeneficiaryTransaction(args)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setTokenToBeneficiary(
    args: SwapperSetTokenToBeneficiaryConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._setTokenToBeneficiaryTransaction(args)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setOracle(args: SwapperSetOracleConfig): Promise<bigint> {
    const gasEstimate = await this._setOracleTransaction(args)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setDefaultScaledOfferFactor(
    args: SwapperSetDefaultScaledOfferFactorConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._setDefaultScaledOfferFactorTransaction(args)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setScaledOfferFactorOverrides(
    args: SwapperSetScaledOfferFactorOverridesConfig,
  ): Promise<bigint> {
    const gasEstimate =
      await this._setScaledOfferFactorOverridesTransaction(args)
    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
interface SwapperGasEstimates extends BaseGasEstimatesMixin {}
applyMixins(SwapperGasEstimates, [BaseGasEstimatesMixin])

class SwapperCallData extends SwapperTransactions {
  constructor(clientArgs: SplitsClientConfig) {
    super({
      transactionType: TransactionType.CallData,
      ...clientArgs,
    })
  }

  async createSwapper(
    createSwapperArgs: CreateSwapperConfig,
  ): Promise<CallData> {
    const callData = await this._createSwapperTransaction(createSwapperArgs)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async uniV3FlashSwap(flashArgs: UniV3FlashSwapConfig): Promise<CallData> {
    const callData = await this._uniV3FlashSwapTransaction(flashArgs)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async universalSwap(flashArgs: UniversalSwapConfig): Promise<CallData> {
    const callData = await this._universalSwapTransaction(flashArgs)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async execCalls(callArgs: SwapperExecCallsConfig): Promise<CallData> {
    const callData = await this._execCallsTransaction(callArgs)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setPaused(args: SwapperPauseConfig): Promise<CallData> {
    const callData = await this._setPausedTransaction(args)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setBeneficiary(args: SwapperSetBeneficiaryConfig): Promise<CallData> {
    const callData = await this._setBeneficiaryTransaction(args)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setTokenToBeneficiary(
    args: SwapperSetTokenToBeneficiaryConfig,
  ): Promise<CallData> {
    const callData = await this._setTokenToBeneficiaryTransaction(args)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setOracle(args: SwapperSetOracleConfig): Promise<CallData> {
    const callData = await this._setOracleTransaction(args)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setDefaultScaledOfferFactor(
    args: SwapperSetDefaultScaledOfferFactorConfig,
  ): Promise<CallData> {
    const callData = await this._setDefaultScaledOfferFactorTransaction(args)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setScaledOfferFactorOverrides(
    args: SwapperSetScaledOfferFactorOverridesConfig,
  ): Promise<CallData> {
    const callData = await this._setScaledOfferFactorOverridesTransaction(args)
    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }
}
