import {
  Address,
  GetContractReturnType,
  Hash,
  Hex,
  Log,
  TypedDataDomain,
  encodeEventTopics,
  fromHex,
  getContract,
  zeroAddress,
} from 'viem'
import { warehouseAbi } from '../constants/abi/warehouse'
import {
  BaseClientMixin,
  BaseGasEstimatesMixin,
  BaseTransactions,
} from './base'
import {
  CallData,
  ReadContractArgs,
  SplitsClientConfig,
  TransactionConfig,
  TransactionFormat,
  WarehouseApproveBySig,
  WarehouseApproveBySigConfig,
  WarehouseApproveConfig,
  WarehouseBatchDepositConfig,
  WarehouseBatchTransferConfig,
  WarehouseBatchWithdrawConfig,
  WarehouseDepositConfig,
  WarehouseInvalidateNonceConfig,
  WarehouseSetOperatorConfig,
  WarehouseSetWithdrawConfig,
  WarehouseTemporaryApproveAndCallBySig,
  WarehouseTemporaryApproveAndCallBySigConfig,
  WarehouseTemporaryApproveAndCallConfig,
  WarehouseTransferConfig,
  WarehouseTransferFromConfig,
  WarehouseWithdrawConfig,
} from '../types'
import {
  TransactionType,
  SPLITS_V2_SUPPORTED_CHAIN_IDS,
  getWarehouseAddress,
  NATIVE_TOKEN_ADDRESS,
} from '../constants'
import { TransactionFailedError } from '../errors'
import { applyMixins } from './mixin'
import { getNumberFromPercent, validateAddress } from '../utils'
import { SplitsPublicClient } from '../types'
type WarehouseAbiType = typeof warehouseAbi

const nativeTokenAddress: Address = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'

class WarehouseTransactions extends BaseTransactions {
  protected readonly _warehouseAbi

  constructor(transactionClientArgs: SplitsClientConfig & TransactionConfig) {
    super({
      supportedChainIds: SPLITS_V2_SUPPORTED_CHAIN_IDS,
      ...transactionClientArgs,
    })

    this._warehouseAbi = warehouseAbi
  }

  protected _getWarehouseContract(
    chainId: number,
  ): GetContractReturnType<WarehouseAbiType, SplitsPublicClient> {
    const publicClient = this._getPublicClient(chainId)
    return getContract({
      address: getWarehouseAddress(),
      abi: warehouseAbi,
      client: publicClient,
    })
  }

  protected async _transfer({
    receiverAddress: receiver,
    tokenAddress: token,
    amount,
    transactionOverrides = {},
  }: WarehouseTransferConfig): Promise<TransactionFormat> {
    validateAddress(receiver)
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'transfer',
      functionArgs: [receiver, fromHex(token, 'bigint'), amount],
      transactionOverrides,
    })

    return result
  }

  protected async _transferFrom({
    senderAddress: sender,
    receiverAddress: receiver,
    tokenAddress: token,
    amount,
    transactionOverrides = {},
  }: WarehouseTransferFromConfig): Promise<TransactionFormat> {
    validateAddress(sender)
    validateAddress(receiver)
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'transferFrom',
      functionArgs: [sender, receiver, fromHex(token, 'bigint'), amount],
      transactionOverrides,
    })

    return result
  }

  protected async _approve({
    spenderAddress: spender,
    tokenAddress: token,
    amount,
    transactionOverrides = {},
  }: WarehouseApproveConfig): Promise<TransactionFormat> {
    validateAddress(spender)
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'approve',
      functionArgs: [spender, fromHex(token, 'bigint'), amount],
      transactionOverrides,
    })

    return result
  }

  protected async _setOperator({
    operatorAddress: operator,
    approved,
    transactionOverrides = {},
  }: WarehouseSetOperatorConfig): Promise<TransactionFormat> {
    validateAddress(operator)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'setOperator',
      functionArgs: [operator, approved],
      transactionOverrides,
    })

    return result
  }

  protected async _invalidateNonce({
    nonce,
    transactionOverrides = {},
  }: WarehouseInvalidateNonceConfig): Promise<TransactionFormat> {
    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'invalidateNonce',
      functionArgs: [nonce],
      transactionOverrides,
    })

    return result
  }

  protected async _temporaryApproveAndCall({
    spenderAddress: spender,
    operator: operator,
    tokenAddress: token,
    amount,
    targetAddress: target,
    data,
    transactionOverrides = {},
  }: WarehouseTemporaryApproveAndCallConfig): Promise<TransactionFormat> {
    validateAddress(spender)
    validateAddress(token)
    validateAddress(target)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'temporaryApproveAndCall',
      functionArgs: [
        spender,
        operator,
        fromHex(token, 'bigint'),
        amount,
        target,
        data,
      ],
      transactionOverrides,
    })

    return result
  }

  protected async _temporaryApproveAndCallBySig({
    ownerAddress: owner,
    spenderAddress: spender,
    operator,
    tokenAddress: token,
    amount,
    targetAddress: target,
    data,
    nonce,
    deadline,
    signature,
    transactionOverrides = {},
  }: WarehouseTemporaryApproveAndCallBySigConfig): Promise<TransactionFormat> {
    validateAddress(owner)
    validateAddress(spender)
    validateAddress(token)
    validateAddress(target)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'temporaryApproveAndCallBySig',
      functionArgs: [
        owner,
        spender,
        operator,
        fromHex(token, 'bigint'),
        amount,
        target,
        data,
        nonce,
        deadline,
        signature,
      ],
      transactionOverrides,
    })

    return result
  }

  protected async _approveBySig({
    ownerAddress: owner,
    spenderAddress: spender,
    operator,
    tokenAddress: token,
    amount,
    nonce,
    deadline,
    signature,
    transactionOverrides = {},
  }: WarehouseApproveBySigConfig): Promise<TransactionFormat> {
    validateAddress(owner)
    validateAddress(spender)
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'approveBySig',
      functionArgs: [
        owner,
        spender,
        operator,
        fromHex(token, 'bigint'),
        amount,
        nonce,
        deadline,
        signature,
      ],
      transactionOverrides,
    })

    return result
  }

  protected async _deposit({
    receiverAddress: receiver,
    tokenAddress: token,
    amount,
    transactionOverrides = {},
  }: WarehouseDepositConfig): Promise<TransactionFormat> {
    validateAddress(receiver)
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'deposit',
      functionArgs: [receiver, token, amount],
      value: token === nativeTokenAddress ? amount : BigInt(0),
      transactionOverrides,
    })

    return result
  }

  protected async _batchDeposit({
    receiversAddresses: receivers,
    tokenAddress: token,
    amounts,
    transactionOverrides = {},
  }: WarehouseBatchDepositConfig): Promise<TransactionFormat> {
    receivers.map((receiver) => validateAddress(receiver))
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'batchDeposit',
      functionArgs: [receivers, token, amounts],
      value: amounts.reduce((a, b) => a + b),
      transactionOverrides,
    })

    return result
  }

  protected async _withdraw({
    ownerAddress: owner,
    tokenAddress: token,
    transactionOverrides = {},
  }: WarehouseWithdrawConfig): Promise<TransactionFormat> {
    validateAddress(owner)
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'withdraw',
      functionArgs: [
        owner,
        token === zeroAddress ? NATIVE_TOKEN_ADDRESS : token,
      ],
      transactionOverrides,
    })

    return result
  }

  protected async _batchWithdraw({
    ownerAddress: owner,
    tokensAddresses: tokens,
    amounts,
    withdrawerAddress: withdrawer,
    transactionOverrides = {},
  }: WarehouseBatchWithdrawConfig): Promise<TransactionFormat> {
    validateAddress(owner)
    tokens.map((token) => validateAddress(token))
    validateAddress(withdrawer)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'withdraw',
      functionArgs: [owner, tokens, amounts, withdrawer],
      transactionOverrides,
    })

    return result
  }

  protected async _batchTransfer({
    receiversAddresses: receivers,
    tokenAddress: token,
    amounts,
    transactionOverrides = {},
  }: WarehouseBatchTransferConfig): Promise<TransactionFormat> {
    receivers.map((receiver) => validateAddress(receiver))
    validateAddress(token)

    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'batchTransfer',
      functionArgs: [receivers, token, amounts],
      transactionOverrides,
    })

    return result
  }

  protected async _setWithdrawConfig({
    incentivePercent: incentive,
    paused,
    transactionOverrides = {},
  }: WarehouseSetWithdrawConfig): Promise<TransactionFormat> {
    if (this._shouldRequireWalletClient) this._requireWalletClient()

    const result = await this._executeContractFunction({
      contractAddress: getWarehouseAddress(),
      contractAbi: warehouseAbi,
      functionName: 'setWithdrawConfig',
      functionArgs: [{ incentive: getNumberFromPercent(incentive), paused }],
      transactionOverrides,
    })

    return result
  }

  protected async _eip712Domain(
    chainId: number,
  ): Promise<{ domain: TypedDataDomain }> {
    const eip712Domain =
      await this._getWarehouseContract(chainId).read.eip712Domain()

    return {
      domain: {
        chainId: Number(eip712Domain[3].toString()),
        name: eip712Domain[1],
        version: eip712Domain[2],
        verifyingContract: eip712Domain[4],
        salt: eip712Domain[5],
      },
    }
  }
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class WarehouseClient extends WarehouseTransactions {
  readonly eventTopics: { [key: string]: Hex[] }
  readonly callData: WarehouseCallData
  readonly estimateGas: WarehouseGasEstimates
  readonly sign: WarehouseSignature

  constructor(clientArgs: SplitsClientConfig) {
    super({
      transactionType: TransactionType.Transaction,
      ...clientArgs,
    })

    this.eventTopics = {
      transfer: [
        encodeEventTopics({
          abi: warehouseAbi,
          eventName: 'Transfer',
        })[0],
      ],
      withdraw: [
        encodeEventTopics({
          abi: warehouseAbi,
          eventName: 'Withdraw',
        })[0],
      ],
      operatorSet: [
        encodeEventTopics({
          abi: warehouseAbi,
          eventName: 'OperatorSet',
        })[0],
      ],
      approval: [
        encodeEventTopics({
          abi: warehouseAbi,
          eventName: 'Approval',
        })[0],
      ],
      nonceInvalidation: [
        encodeEventTopics({
          abi: warehouseAbi,
          eventName: 'NonceInvalidation',
        })[0],
      ],
      withdrawConfigUpdated: [
        encodeEventTopics({
          abi: warehouseAbi,
          eventName: 'WithdrawConfigUpdated',
        })[0],
      ],
    }

    this.callData = new WarehouseCallData(clientArgs)
    this.estimateGas = new WarehouseGasEstimates(clientArgs)
    this.sign = new WarehouseSignature(clientArgs)
  }

  // ERC6909 Metadata
  async getName({
    tokenAddress,
    chainId,
  }: ReadContractArgs & { tokenAddress: Address }): Promise<{
    name: string
  }> {
    validateAddress(tokenAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    const name = await this._getWarehouseContract(functionChainId).read.name([
      fromHex(tokenAddress, 'bigint'),
    ])

    return {
      name,
    }
  }

  async getSymbol({
    tokenAddress,
    chainId,
  }: ReadContractArgs & { tokenAddress: Address }): Promise<{
    symbol: string
  }> {
    validateAddress(tokenAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const symbol = await this._getWarehouseContract(
      functionChainId,
    ).read.symbol([fromHex(tokenAddress, 'bigint')])

    return {
      symbol,
    }
  }

  async getDecimals({
    tokenAddress,
    chainId,
  }: ReadContractArgs & { tokenAddress: Address }): Promise<{
    decimals: number
  }> {
    validateAddress(tokenAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const decimals = await this._getWarehouseContract(
      functionChainId,
    ).read.decimals([fromHex(tokenAddress, 'bigint')])

    return {
      decimals,
    }
  }

  // Warehouse withdraw config
  async getWithdrawConfig({
    userAddress,
    chainId,
  }: ReadContractArgs & {
    userAddress: Address
  }): Promise<{ withdrawConfig: { incentive: number; paused: boolean } }> {
    validateAddress(userAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const config = await this._getWarehouseContract(
      functionChainId,
    ).read.withdrawConfig([userAddress])

    return {
      withdrawConfig: {
        incentive: config[0],
        paused: config[1],
      },
    }
  }

  // ERC6909X nonce
  async isValidNonce({
    userAddress,
    userNonce,
    chainId,
  }: ReadContractArgs & {
    userAddress: Address
    userNonce: bigint
  }): Promise<{ isValidNonce: boolean }> {
    validateAddress(userAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const isValidNonce = await this._getWarehouseContract(
      functionChainId,
    ).read.isValidNonce([userAddress, userNonce])

    return {
      isValidNonce,
    }
  }

  async eip712Domain(
    args?: ReadContractArgs,
  ): Promise<{ domain: TypedDataDomain }> {
    const chainId = args?.chainId
    const functionChainId = this._getReadOnlyFunctionChainId(chainId)
    return this._eip712Domain(functionChainId)
  }

  // ERC6909 read
  async isOperator({
    ownerAddress,
    operatorAddress,
    chainId,
  }: ReadContractArgs & {
    ownerAddress: Address
    operatorAddress: Address
  }): Promise<{ isOperator: boolean }> {
    validateAddress(ownerAddress)
    validateAddress(operatorAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const isOperator = await this._getWarehouseContract(
      functionChainId,
    ).read.isOperator([ownerAddress, operatorAddress])

    return {
      isOperator,
    }
  }

  async balanceOf({
    ownerAddress,
    tokenAddress,
    chainId,
  }: ReadContractArgs & {
    ownerAddress: Address
    tokenAddress: Address
  }): Promise<{ balance: bigint }> {
    validateAddress(ownerAddress)
    validateAddress(tokenAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const balance = await this._getWarehouseContract(
      functionChainId,
    ).read.balanceOf([ownerAddress, fromHex(tokenAddress, 'bigint')])

    return {
      balance,
    }
  }

  async allowance({
    ownerAddress,
    spenderAddress,
    tokenAddress,
    chainId,
  }: ReadContractArgs & {
    ownerAddress: Address
    spenderAddress: Address
    tokenAddress: Address
  }): Promise<{ allowance: bigint }> {
    validateAddress(ownerAddress)
    validateAddress(spenderAddress)
    validateAddress(tokenAddress)

    const functionChainId = this._getReadOnlyFunctionChainId(chainId)

    const allowance = await this._getWarehouseContract(
      functionChainId,
    ).read.allowance([
      ownerAddress,
      spenderAddress,
      fromHex(tokenAddress, 'bigint'),
    ])

    return {
      allowance,
    }
  }

  // erc6909 write
  async transfer(
    transferArgs: WarehouseTransferConfig,
  ): Promise<{ event: Log }> {
    const txHash = await this._transfer(transferArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.transfer,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async transferFrom(
    transferFromArgs: WarehouseTransferFromConfig,
  ): Promise<{ event: Log }> {
    const txHash = await this._transferFrom(transferFromArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.transfer,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async approve(approveArgs: WarehouseApproveConfig): Promise<{ event: Log }> {
    const txHash = await this._approve(approveArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.approval,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async setOperator(
    setOperatorArgs: WarehouseSetOperatorConfig,
  ): Promise<{ event: Log }> {
    const txHash = await this._setOperator(setOperatorArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.operatorSet,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async invalidateNonce(
    invalidateNonceArgs: WarehouseInvalidateNonceConfig,
  ): Promise<{ event: Log }> {
    const txHash = await this._invalidateNonce(invalidateNonceArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.nonceInvalidation,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async temporaryApproveAndCall(
    temporaryApproveAndCallArgs: WarehouseTemporaryApproveAndCallConfig,
  ): Promise<{ txHash: Hex }> {
    const txHash = await this._temporaryApproveAndCall(
      temporaryApproveAndCallArgs,
    )

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async temporaryApproveAndCallBySig(
    temporaryApproveAndCallBySigArgs: WarehouseTemporaryApproveAndCallBySigConfig,
  ): Promise<{ txHash: Hex }> {
    const txHash = await this._temporaryApproveAndCallBySig(
      temporaryApproveAndCallBySigArgs,
    )

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async approveBySig(
    approveBySigArgs: WarehouseApproveBySigConfig,
  ): Promise<{ event: Log }> {
    const txHash = await this._approveBySig(approveBySigArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    let eventTopics: Hex[] = []
    if (approveBySigArgs.operator) {
      eventTopics = this.eventTopics.operatorSet
    } else {
      eventTopics = this.eventTopics.approval
    }

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async deposit(depositArgs: WarehouseDepositConfig): Promise<{ event: Log }> {
    const txHash = await this._deposit(depositArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.transfer,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async batchDeposit(
    batchDepositArgs: WarehouseBatchDepositConfig,
  ): Promise<{ events: Log[] }> {
    const txHash = await this._batchDeposit(batchDepositArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.transfer,
    })

    if (events.length > 0) {
      return {
        events,
      }
    }

    throw new TransactionFailedError()
  }

  async _submitWithdrawTransaction(
    withdrawArgs: WarehouseWithdrawConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._withdraw(withdrawArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async withdraw(
    withdrawArgs: WarehouseWithdrawConfig,
  ): Promise<{ event: Log }> {
    const { txHash } = await this._submitWithdrawTransaction(withdrawArgs)

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.withdraw,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }

  async _submitBatchWithdrawTransaction(
    batchWithdrawArgs: WarehouseBatchWithdrawConfig,
  ): Promise<{
    txHash: Hash
  }> {
    const txHash = await this._batchWithdraw(batchWithdrawArgs)
    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    return { txHash }
  }

  async batchWithdraw(
    batchWithdrawArgs: WarehouseBatchWithdrawConfig,
  ): Promise<{ events: Log[] }> {
    const { txHash } =
      await this._submitBatchWithdrawTransaction(batchWithdrawArgs)

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.withdraw,
    })

    if (events.length > 0) {
      return {
        events,
      }
    }

    throw new TransactionFailedError()
  }

  async batchTransfer(
    batchTransferArgs: WarehouseBatchTransferConfig,
  ): Promise<{ events: Log[] }> {
    const txHash = await this._batchTransfer(batchTransferArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.transfer,
    })

    if (events.length > 0) {
      return {
        events,
      }
    }

    throw new TransactionFailedError()
  }

  async setWithdrawConfig(
    setConfigArgs: WarehouseSetWithdrawConfig,
  ): Promise<{ event: Log }> {
    const txHash = await this._setWithdrawConfig(setConfigArgs)

    if (!this._isContractTransaction(txHash))
      throw new Error('Invalid response')

    const events = await this.getTransactionEvents({
      txHash,
      eventTopics: this.eventTopics.withdrawConfigUpdated,
    })

    const event = events.length > 0 ? events[0] : undefined

    if (event) {
      return {
        event,
      }
    }

    throw new TransactionFailedError()
  }
}

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

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

  async transfer(transferArgs: WarehouseTransferConfig): Promise<bigint> {
    const gasEstimate = await this._transfer(transferArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async transferFrom(
    transferFromArgs: WarehouseTransferFromConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._transferFrom(transferFromArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async approve(approveArgs: WarehouseApproveConfig): Promise<bigint> {
    const gasEstimate = await this._approve(approveArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setOperator(
    setOperatorArgs: WarehouseSetOperatorConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._setOperator(setOperatorArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async invalidateNonce(
    invalidateNonceArgs: WarehouseInvalidateNonceConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._invalidateNonce(invalidateNonceArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async temporaryApproveAndCall(
    temporaryApproveAndCallArgs: WarehouseTemporaryApproveAndCallConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._temporaryApproveAndCall(
      temporaryApproveAndCallArgs,
    )

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async temporaryApproveAndCallBySig(
    temporaryApproveAndCallBySigArgs: WarehouseTemporaryApproveAndCallBySigConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._temporaryApproveAndCallBySig(
      temporaryApproveAndCallBySigArgs,
    )

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async approveBySig(
    approveBySigArgs: WarehouseApproveBySigConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._approveBySig(approveBySigArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async deposit(depositArgs: WarehouseDepositConfig): Promise<bigint> {
    const gasEstimate = await this._deposit(depositArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async batchDeposit(
    batchDepositArgs: WarehouseBatchDepositConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._batchDeposit(batchDepositArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async withdraw(withdrawArgs: WarehouseWithdrawConfig): Promise<bigint> {
    const gasEstimate = await this._withdraw(withdrawArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async batchWithdraw(
    batchWithdrawArgs: WarehouseBatchWithdrawConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._batchWithdraw(batchWithdrawArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async batchTransfer(
    batchTransferArgs: WarehouseBatchTransferConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._batchTransfer(batchTransferArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }

  async setWithdrawConfig(
    setConfigArgs: WarehouseSetWithdrawConfig,
  ): Promise<bigint> {
    const gasEstimate = await this._setWithdrawConfig(setConfigArgs)

    if (!this._isBigInt(gasEstimate)) throw new Error('Invalid response')

    return gasEstimate
  }
}

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

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

  async transfer(transferArgs: WarehouseTransferConfig): Promise<CallData> {
    const callData = await this._transfer(transferArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async transferFrom(
    transferFromArgs: WarehouseTransferFromConfig,
  ): Promise<CallData> {
    const callData = await this._transferFrom(transferFromArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async approve(approveArgs: WarehouseApproveConfig): Promise<CallData> {
    const callData = await this._approve(approveArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setOperator(
    setOperatorArgs: WarehouseSetOperatorConfig,
  ): Promise<CallData> {
    const callData = await this._setOperator(setOperatorArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async invalidateNonce(
    invalidateNonceArgs: WarehouseInvalidateNonceConfig,
  ): Promise<CallData> {
    const callData = await this._invalidateNonce(invalidateNonceArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async temporaryApproveAndCall(
    temporaryApproveAndCallArgs: WarehouseTemporaryApproveAndCallConfig,
  ): Promise<CallData> {
    const callData = await this._temporaryApproveAndCall(
      temporaryApproveAndCallArgs,
    )

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async temporaryApproveAndCallBySig(
    temporaryApproveAndCallBySigArgs: WarehouseTemporaryApproveAndCallBySigConfig,
  ): Promise<CallData> {
    const callData = await this._temporaryApproveAndCallBySig(
      temporaryApproveAndCallBySigArgs,
    )

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async approveBySig(
    approveBySigArgs: WarehouseApproveBySigConfig,
  ): Promise<CallData> {
    const callData = await this._approveBySig(approveBySigArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async deposit(depositArgs: WarehouseDepositConfig): Promise<CallData> {
    const callData = await this._deposit(depositArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async batchDeposit(
    batchDepositArgs: WarehouseBatchDepositConfig,
  ): Promise<CallData> {
    const callData = await this._batchDeposit(batchDepositArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async withdraw(withdrawArgs: WarehouseWithdrawConfig): Promise<CallData> {
    const callData = await this._withdraw(withdrawArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async batchWithdraw(
    withdrawArgs: WarehouseWithdrawConfig,
  ): Promise<CallData> {
    const callData = await this._withdraw(withdrawArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async batchTransfer(
    batchTransferArgs: WarehouseBatchTransferConfig,
  ): Promise<CallData> {
    const callData = await this._batchTransfer(batchTransferArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }

  async setWithdrawConfig(
    setConfigArgs: WarehouseSetWithdrawConfig,
  ): Promise<CallData> {
    const callData = await this._setWithdrawConfig(setConfigArgs)

    if (!this._isCallData(callData)) throw new Error('Invalid response')

    return callData
  }
}

class WarehouseSignature extends WarehouseTransactions {
  constructor(clientArgs: SplitsClientConfig) {
    super({
      transactionType: TransactionType.Signature,
      ...clientArgs,
    })
  }

  async approveBySig(
    approveBySigArgs: WarehouseApproveBySig,
  ): Promise<WarehouseApproveBySigConfig> {
    validateAddress(approveBySigArgs.spenderAddress)
    validateAddress(approveBySigArgs.tokenAddress)
    this._requireWalletClient()

    const { domain } = await this._eip712Domain(this._walletClient!.chain!.id)

    this._requireWalletClient()

    const signature = await this._walletClient!.signTypedData({
      account: this._walletClient!.account!,
      domain,
      types: SigTypes,
      primaryType: 'ERC6909XApproveAndCall',
      message: {
        owner: this._walletClient!.account!.address,
        spender: approveBySigArgs.spenderAddress,
        temporary: false,
        operator: approveBySigArgs.operator,
        id: fromHex(approveBySigArgs.tokenAddress, 'bigint'),
        amount: approveBySigArgs.amount,
        target: zeroAddress,
        data: '' as Hex,
        nonce: approveBySigArgs.nonce,
        deadline: approveBySigArgs.deadline,
      },
    })

    if (!signature) throw new Error('Error in signing data')

    return {
      ownerAddress: this._walletClient!.account!.address as Address,
      signature,
      ...approveBySigArgs,
    }
  }

  async temporaryApproveAndCallBySig(
    temporaryApproveAndCallBySigArgs: WarehouseTemporaryApproveAndCallBySig,
  ): Promise<WarehouseApproveBySigConfig> {
    validateAddress(temporaryApproveAndCallBySigArgs.spenderAddress)
    validateAddress(temporaryApproveAndCallBySigArgs.tokenAddress)
    validateAddress(temporaryApproveAndCallBySigArgs.targetAddress)
    this._requireWalletClient()

    const { domain } = await this._eip712Domain(this._walletClient!.chain!.id)

    const signature = await this._walletClient!.signTypedData({
      account: this._walletClient!.account!,
      domain,
      types: SigTypes,
      primaryType: 'ERC6909XApproveAndCall',
      message: {
        owner: this._walletClient!.account!.address,
        spender: temporaryApproveAndCallBySigArgs.spenderAddress,
        temporary: true,
        operator: temporaryApproveAndCallBySigArgs.operator,
        id: fromHex(temporaryApproveAndCallBySigArgs.tokenAddress, 'bigint'),
        amount: temporaryApproveAndCallBySigArgs.amount,
        target: temporaryApproveAndCallBySigArgs.targetAddress,
        data: temporaryApproveAndCallBySigArgs.data,
        nonce: temporaryApproveAndCallBySigArgs.nonce,
        deadline: temporaryApproveAndCallBySigArgs.deadline,
      },
    })

    if (!signature) throw new Error('Error in signing data')

    return {
      ownerAddress: this._walletClient!.account!.address as Address,
      signature,
      ...temporaryApproveAndCallBySigArgs,
    }
  }
}

const SigTypes = {
  ERC6909XApproveAndCall: [
    {
      name: 'temporary',
      type: 'bool',
    },
    {
      name: 'owner',
      type: 'address',
    },
    {
      name: 'spender',
      type: 'address',
    },
    {
      name: 'operator',
      type: 'bool',
    },
    {
      name: 'id',
      type: 'uint256',
    },
    {
      name: 'amount',
      type: 'uint256',
    },
    {
      name: 'target',
      type: 'address',
    },
    {
      name: 'data',
      type: 'bytes',
    },
    {
      name: 'nonce',
      type: 'uint256',
    },
    {
      name: 'deadline',
      type: 'uint48',
    },
  ],
} as const
