import {
  type Account,
  type Address,
  encodeAbiParameters,
  encodeFunctionData,
  type Hex,
  zeroHash,
} from 'viem'
import {
  readContract as viem_readContract,
  sendTransaction as viem_sendTransaction,
  sendTransactionSync as viem_sendTransactionSync,
} from 'viem/actions'
import { Abis, Actions, Bytes, PublicKey, Secp256k1, TokenId } from 'viem/tempo'
import { Abis as ZoneAbis } from 'viem/tempo/zones'
import { parseAccount } from 'viem/utils'
import { getConnectorClient } from '../../actions/getConnectorClient.js'
import type { Config } from '../../createConfig.js'
import type {
  ChainIdParameter,
  ConnectorParameter,
} from '../../types/properties.js'
import type { PartialBy, UnionLooseOmit } from '../../types/utils.js'
import type { QueryOptions, QueryParameter } from './utils.js'
import { filterQueryOptions } from './utils.js'

/**
 * Gets information about the currently stored zone authorization token.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * await Actions.zone.signAuthorizationToken(config, {
 *   chainId: zoneChain.id,
 * })
 *
 * const info = await Actions.zone.getAuthorizationTokenInfo(config, {
 *   chainId: zoneChain.id,
 * })
 *
 * console.log(info.expiresAt)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The authorization token info.
 */
export function getAuthorizationTokenInfo<config extends Config>(
  config: config,
  parameters: getAuthorizationTokenInfo.Parameters<config>,
): Promise<getAuthorizationTokenInfo.ReturnValue> {
  const client = config.getClient({ chainId: parameters.chainId })
  return Actions.zone.getAuthorizationTokenInfo(client)
}

export namespace getAuthorizationTokenInfo {
  export type Parameters<config extends Config> = ChainIdParameter<config>

  export type ReturnValue = Actions.zone.getAuthorizationTokenInfo.ReturnType

  export type ErrorType = Actions.zone.getAuthorizationTokenInfo.ErrorType

  export function queryKey<config extends Config>(
    parameters: Parameters<config>,
  ) {
    return [
      'getAuthorizationTokenInfo',
      filterQueryOptions(parameters),
    ] as const
  }

  export type QueryKey<config extends Config> = ReturnType<
    typeof queryKey<config>
  >

  export function queryOptions<config extends Config, selectData = ReturnValue>(
    config: Config,
    parameters: queryOptions.Parameters<config, selectData>,
  ): queryOptions.ReturnValue<config, selectData> {
    const { query, ...rest } = parameters
    return {
      ...query,
      enabled: Boolean(query?.enabled ?? true),
      queryKey: queryKey(rest),
      async queryFn(context) {
        const [, parameters] = context.queryKey
        return await getAuthorizationTokenInfo(config, parameters)
      },
    }
  }

  export declare namespace queryOptions {
    export type Parameters<
      config extends Config,
      selectData = getAuthorizationTokenInfo.ReturnValue,
    > = getAuthorizationTokenInfo.Parameters<config> &
      QueryParameter<
        getAuthorizationTokenInfo.ReturnValue,
        getAuthorizationTokenInfo.ErrorType,
        selectData,
        getAuthorizationTokenInfo.QueryKey<config>
      >

    export type ReturnValue<
      config extends Config,
      selectData = getAuthorizationTokenInfo.ReturnValue,
    > = QueryOptions<
      getAuthorizationTokenInfo.ReturnValue,
      getAuthorizationTokenInfo.ErrorType,
      selectData,
      getAuthorizationTokenInfo.QueryKey<config>
    >
  }
}

/**
 * Gets deposit processing status for a Tempo block number.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * await Actions.zone.signAuthorizationToken(config, {
 *   chainId: zoneChain.id,
 * })
 *
 * const status = await Actions.zone.getDepositStatus(config, {
 *   chainId: zoneChain.id,
 *   tempoBlockNumber: 42n,
 * })
 *
 * console.log(status.processed)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The deposit status.
 */
export function getDepositStatus<config extends Config>(
  config: config,
  parameters: getDepositStatus.Parameters<config>,
): Promise<getDepositStatus.ReturnValue> {
  const { chainId, ...rest } = parameters
  const client = config.getClient({ chainId })
  return Actions.zone.getDepositStatus(client, rest)
}

export namespace getDepositStatus {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    Actions.zone.getDepositStatus.Parameters

  export type ReturnValue = Actions.zone.getDepositStatus.ReturnType

  export type ErrorType = Actions.zone.getDepositStatus.ErrorType

  export function queryKey<config extends Config>(
    parameters: PartialBy<Parameters<config>, 'tempoBlockNumber'>,
  ) {
    return ['getDepositStatus', filterQueryOptions(parameters)] as const
  }

  export type QueryKey<config extends Config> = ReturnType<
    typeof queryKey<config>
  >

  export function queryOptions<config extends Config, selectData = ReturnValue>(
    config: Config,
    parameters: queryOptions.Parameters<config, selectData>,
  ): queryOptions.ReturnValue<config, selectData> {
    const { query, ...rest } = parameters
    return {
      ...query,
      enabled: Boolean(
        rest.tempoBlockNumber !== undefined && (query?.enabled ?? true),
      ),
      queryKey: queryKey(rest),
      async queryFn(context) {
        const [, { tempoBlockNumber, ...parameters }] = context.queryKey
        if (tempoBlockNumber === undefined)
          throw new Error('tempoBlockNumber is required.')
        return await getDepositStatus(config, {
          ...parameters,
          tempoBlockNumber,
        })
      },
    }
  }

  export declare namespace queryOptions {
    export type Parameters<
      config extends Config,
      selectData = getDepositStatus.ReturnValue,
    > = PartialBy<getDepositStatus.Parameters<config>, 'tempoBlockNumber'> &
      QueryParameter<
        getDepositStatus.ReturnValue,
        getDepositStatus.ErrorType,
        selectData,
        getDepositStatus.QueryKey<config>
      >

    export type ReturnValue<
      config extends Config,
      selectData = getDepositStatus.ReturnValue,
    > = QueryOptions<
      getDepositStatus.ReturnValue,
      getDepositStatus.ErrorType,
      selectData,
      getDepositStatus.QueryKey<config>
    >
  }
}

/**
 * Gets the withdrawal fee for a given gas limit.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions } from '@wagmi/core/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const config = createConfig({
 *   chains: [zoneChain],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const fee = await Actions.zone.getWithdrawalFee(config, {
 *   chainId: zoneChain.id,
 *   gas: 21_000n,
 * })
 *
 * console.log(fee)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The withdrawal fee.
 */
export function getWithdrawalFee<config extends Config>(
  config: config,
  parameters: getWithdrawalFee.Parameters<config>,
): Promise<getWithdrawalFee.ReturnValue> {
  const { chainId, ...rest } = parameters
  const client = config.getClient({ chainId })
  return Actions.zone.getWithdrawalFee(client, rest)
}

export namespace getWithdrawalFee {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    Actions.zone.getWithdrawalFee.Parameters

  export type ReturnValue = Actions.zone.getWithdrawalFee.ReturnType

  export type ErrorType = Actions.zone.getWithdrawalFee.ErrorType

  export function queryKey<config extends Config>(
    parameters: Parameters<config>,
  ) {
    return ['getWithdrawalFee', filterQueryOptions(parameters)] as const
  }

  export type QueryKey<config extends Config> = ReturnType<
    typeof queryKey<config>
  >

  export function queryOptions<config extends Config, selectData = ReturnValue>(
    config: Config,
    parameters: queryOptions.Parameters<config, selectData>,
  ): queryOptions.ReturnValue<config, selectData> {
    const { query, ...rest } = parameters
    return {
      ...query,
      enabled: Boolean(query?.enabled ?? true),
      queryKey: queryKey(rest),
      async queryFn(context) {
        const [, parameters] = context.queryKey
        return await getWithdrawalFee(config, parameters)
      },
    }
  }

  export declare namespace queryOptions {
    export type Parameters<
      config extends Config,
      selectData = getWithdrawalFee.ReturnValue,
    > = getWithdrawalFee.Parameters<config> &
      QueryParameter<
        getWithdrawalFee.ReturnValue,
        getWithdrawalFee.ErrorType,
        selectData,
        getWithdrawalFee.QueryKey<config>
      >

    export type ReturnValue<
      config extends Config,
      selectData = getWithdrawalFee.ReturnValue,
    > = QueryOptions<
      getWithdrawalFee.ReturnValue,
      getWithdrawalFee.ErrorType,
      selectData,
      getWithdrawalFee.QueryKey<config>
    >
  }
}

/**
 * Gets the current zone metadata.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions } from '@wagmi/core/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const config = createConfig({
 *   chains: [zoneChain],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const info = await Actions.zone.getZoneInfo(config, {
 *   chainId: zoneChain.id,
 * })
 *
 * console.log(info.zoneId)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The zone metadata.
 */
export function getZoneInfo<config extends Config>(
  config: config,
  parameters: getZoneInfo.Parameters<config>,
): Promise<getZoneInfo.ReturnValue> {
  const client = config.getClient({ chainId: parameters.chainId })
  return Actions.zone.getZoneInfo(client)
}

export namespace getZoneInfo {
  export type Parameters<config extends Config> = ChainIdParameter<config>

  export type ReturnValue = Actions.zone.getZoneInfo.ReturnType

  export type ErrorType = Actions.zone.getZoneInfo.ErrorType

  export function queryKey<config extends Config>(
    parameters: Parameters<config>,
  ) {
    return ['getZoneInfo', filterQueryOptions(parameters)] as const
  }

  export type QueryKey<config extends Config> = ReturnType<
    typeof queryKey<config>
  >

  export function queryOptions<config extends Config, selectData = ReturnValue>(
    config: Config,
    parameters: queryOptions.Parameters<config, selectData>,
  ): queryOptions.ReturnValue<config, selectData> {
    const { query, ...rest } = parameters
    return {
      ...query,
      enabled: Boolean(query?.enabled ?? true),
      queryKey: queryKey(rest),
      async queryFn(context) {
        const [, parameters] = context.queryKey
        return await getZoneInfo(config, parameters)
      },
    }
  }

  export declare namespace queryOptions {
    export type Parameters<
      config extends Config,
      selectData = getZoneInfo.ReturnValue,
    > = getZoneInfo.Parameters<config> &
      QueryParameter<
        getZoneInfo.ReturnValue,
        getZoneInfo.ErrorType,
        selectData,
        getZoneInfo.QueryKey<config>
      >

    export type ReturnValue<
      config extends Config,
      selectData = getZoneInfo.ReturnValue,
    > = QueryOptions<
      getZoneInfo.ReturnValue,
      getZoneInfo.ErrorType,
      selectData,
      getZoneInfo.QueryKey<config>
    >
  }
}

/**
 * Signs and stores a zone authorization token for the configured zone transport.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const result = await Actions.zone.signAuthorizationToken(config, {
 *   chainId: zoneChain.id,
 * })
 *
 * console.log(result.token)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The authentication payload and serialized token.
 */
export async function signAuthorizationToken<config extends Config>(
  config: config,
  parameters: signAuthorizationToken.Parameters<config>,
): Promise<signAuthorizationToken.ReturnValue> {
  const { account, chainId, connector, ...rest } = parameters
  const client = await getZoneWalletClient(config, {
    account,
    chainId,
    connector,
  })
  return Actions.zone.signAuthorizationToken(client, rest as never)
}

export declare namespace signAuthorizationToken {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter & {
      account?: Address | Account | undefined
    } & UnionLooseOmit<
      Actions.zone.signAuthorizationToken.Parameters<Account>,
      'account' | 'chain'
    >

  export type ReturnValue = Actions.zone.signAuthorizationToken.ReturnType

  export type ErrorType = Actions.zone.signAuthorizationToken.ErrorType
}

/**
 * Deposits tokens into a zone on the parent Tempo chain.
 *
 * @example
 * ```ts
 * import { createConfig, http } from '@wagmi/core'
 * import { tempoModerato } from '@wagmi/core/chains'
 * import { Actions } from '@wagmi/core/tempo'
 *
 * const config = createConfig({
 *   chains: [tempoModerato],
 *   transports: {
 *     [tempoModerato.id]: http(),
 *   },
 * })
 *
 * const hash = await Actions.zone.deposit(config, {
 *   amount: 1_000_000n,
 *   token: '0x20c0000000000000000000000000000000000001',
 *   zoneId: 7,
 * })
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns Transaction hash.
 */
export async function deposit<config extends Config>(
  config: config,
  parameters: deposit.Parameters<config>,
): Promise<deposit.ReturnValue> {
  const { account, chainId, connector, ...rest } = parameters
  const client = await getConnectorClient(config, {
    account,
    assertChainId: false,
    chainId,
    connector,
  })

  const resolvedChainId = chainId ?? client.chain?.id
  if (!resolvedChainId) throw new Error('`chainId` is required.')

  const account_ = account ?? client.account
  if (!account_) throw new Error('`account` is required.')

  const accountAddress = parseAccount(account_).address
  const {
    amount,
    memo = zeroHash,
    recipient = accountAddress,
    token,
    zoneId,
    ...tx
  } = rest
  const { address: portalAddress } = resolvePortal(
    config,
    resolvedChainId,
    zoneId,
  )
  const tokenAddress = TokenId.toAddress(token)

  return viem_sendTransaction(client, {
    ...tx,
    calls: [
      {
        data: encodeFunctionData({
          abi: Abis.tip20,
          functionName: 'approve',
          args: [portalAddress, amount],
        }),
        to: tokenAddress,
      },
      {
        data: encodeFunctionData({
          abi: ZoneAbis.zonePortal,
          functionName: 'deposit',
          args: [tokenAddress, recipient, amount, memo],
        }),
        to: portalAddress,
      },
    ],
  } as never) as never
}

export declare namespace deposit {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.deposit.Parameters<config['chains'][number], Account>,
      'chain'
    >

  export type ReturnValue = Actions.zone.deposit.ReturnValue

  export type ErrorType = Actions.zone.deposit.ErrorType
}

/**
 * Deposits tokens into a zone on the parent Tempo chain.
 *
 * Note: This is a synchronous action that waits for the transaction to
 * be included on a block before returning a response.
 *
 * @example
 * ```ts
 * import { createConfig, http } from '@wagmi/core'
 * import { tempoModerato } from '@wagmi/core/chains'
 * import { Actions } from '@wagmi/core/tempo'
 *
 * const config = createConfig({
 *   chains: [tempoModerato],
 *   transports: {
 *     [tempoModerato.id]: http(),
 *   },
 * })
 *
 * const result = await Actions.zone.depositSync(config, {
 *   amount: 1_000_000n,
 *   token: '0x20c0000000000000000000000000000000000001',
 *   zoneId: 7,
 * })
 *
 * console.log(result.receipt.transactionHash)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The transaction receipt.
 */
export async function depositSync<config extends Config>(
  config: config,
  parameters: depositSync.Parameters<config>,
): Promise<depositSync.ReturnValue> {
  const {
    account,
    chainId,
    connector,
    throwOnReceiptRevert = true,
    ...rest
  } = parameters
  const client = await getConnectorClient(config, {
    account,
    assertChainId: false,
    chainId,
    connector,
  })

  const resolvedChainId = chainId ?? client.chain?.id
  if (!resolvedChainId) throw new Error('`chainId` is required.')

  const account_ = account ?? client.account
  if (!account_) throw new Error('`account` is required.')

  const accountAddress = parseAccount(account_).address
  const {
    amount,
    memo = zeroHash,
    recipient = accountAddress,
    token,
    zoneId,
    ...tx
  } = rest
  const { address: portalAddress } = resolvePortal(
    config,
    resolvedChainId,
    zoneId,
  )
  const tokenAddress = TokenId.toAddress(token)

  const receipt = await viem_sendTransactionSync(client, {
    ...tx,
    calls: [
      {
        data: encodeFunctionData({
          abi: Abis.tip20,
          functionName: 'approve',
          args: [portalAddress, amount],
        }),
        to: tokenAddress,
      },
      {
        data: encodeFunctionData({
          abi: ZoneAbis.zonePortal,
          functionName: 'deposit',
          args: [tokenAddress, recipient, amount, memo],
        }),
        to: portalAddress,
      },
    ],
    throwOnReceiptRevert,
  } as never)

  return { receipt }
}

export declare namespace depositSync {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.depositSync.Parameters<config['chains'][number], Account>,
      'chain'
    >

  export type ReturnValue = Actions.zone.depositSync.ReturnValue

  export type ErrorType = Actions.zone.depositSync.ErrorType
}

/**
 * Deposits tokens into a zone on the parent Tempo chain with an encrypted
 * recipient and memo.
 *
 * @example
 * ```ts
 * import { createConfig, http } from '@wagmi/core'
 * import { tempoModerato } from '@wagmi/core/chains'
 * import { Actions } from '@wagmi/core/tempo'
 *
 * const config = createConfig({
 *   chains: [tempoModerato],
 *   transports: {
 *     [tempoModerato.id]: http(),
 *   },
 * })
 *
 * const hash = await Actions.zone.encryptedDeposit(config, {
 *   amount: 1_000_000n,
 *   token: '0x20c0000000000000000000000000000000000001',
 *   zoneId: 7,
 * })
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns Transaction hash.
 */
export async function encryptedDeposit<config extends Config>(
  config: config,
  parameters: encryptedDeposit.Parameters<config>,
): Promise<encryptedDeposit.ReturnValue> {
  const { account, chainId, connector, ...rest } = parameters
  const client = await getConnectorClient(config, {
    account,
    assertChainId: false,
    chainId,
    connector,
  })

  const resolvedChainId = chainId ?? client.chain?.id
  if (!resolvedChainId) throw new Error('`chainId` is required.')

  const account_ = account ?? client.account
  if (!account_) throw new Error('`account` is required.')

  const accountAddress = parseAccount(account_).address
  const {
    amount,
    memo,
    recipient = accountAddress,
    token,
    zoneId,
    ...tx
  } = rest
  const portal = resolvePortal(config, resolvedChainId, zoneId)
  const portalAddress = portal.address
  const tokenAddress = TokenId.toAddress(token)
  const [publicKey, keyIndex] =
    portal.sequencerEncryptionKey && portal.encryptionKeyCount !== undefined
      ? [portal.sequencerEncryptionKey, portal.encryptionKeyCount]
      : await Promise.all([
          viem_readContract(client, {
            address: portalAddress,
            abi: ZoneAbis.zonePortal,
            functionName: 'sequencerEncryptionKey',
          }).then(([x, yParity]) => ({ x, yParity: Number(yParity) })),
          viem_readContract(client, {
            address: portalAddress,
            abi: ZoneAbis.zonePortal,
            functionName: 'encryptionKeyCount',
          }),
        ])
  if (keyIndex === 0n)
    throw new Error('No sequencer encryption key configured.')
  const encrypted = await encryptDepositPayload(publicKey, recipient, memo)

  return viem_sendTransaction(client, {
    ...tx,
    calls: [
      {
        data: encodeFunctionData({
          abi: Abis.tip20,
          functionName: 'approve',
          args: [portalAddress, amount],
        }),
        to: tokenAddress,
      },
      {
        data: encodeFunctionData({
          abi: ZoneAbis.zonePortal,
          functionName: 'depositEncrypted',
          args: [tokenAddress, amount, keyIndex - 1n, encrypted],
        }),
        to: portalAddress,
      },
    ],
  } as never) as never
}

export declare namespace encryptedDeposit {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.encryptedDeposit.Parameters<
        config['chains'][number],
        Account
      >,
      'chain'
    >

  export type ReturnValue = Actions.zone.encryptedDeposit.ReturnValue

  export type ErrorType = Actions.zone.encryptedDeposit.ErrorType
}

/**
 * Deposits tokens into a zone on the parent Tempo chain with an encrypted
 * recipient and memo.
 *
 * Note: This is a synchronous action that waits for the transaction to
 * be included on a block before returning a response.
 *
 * @example
 * ```ts
 * import { createConfig, http } from '@wagmi/core'
 * import { tempoModerato } from '@wagmi/core/chains'
 * import { Actions } from '@wagmi/core/tempo'
 *
 * const config = createConfig({
 *   chains: [tempoModerato],
 *   transports: {
 *     [tempoModerato.id]: http(),
 *   },
 * })
 *
 * const result = await Actions.zone.encryptedDepositSync(config, {
 *   amount: 1_000_000n,
 *   token: '0x20c0000000000000000000000000000000000001',
 *   zoneId: 7,
 * })
 *
 * console.log(result.receipt.transactionHash)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The transaction receipt.
 */
export async function encryptedDepositSync<config extends Config>(
  config: config,
  parameters: encryptedDepositSync.Parameters<config>,
): Promise<encryptedDepositSync.ReturnValue> {
  const {
    account,
    chainId,
    connector,
    throwOnReceiptRevert = true,
    ...rest
  } = parameters
  const client = await getConnectorClient(config, {
    account,
    assertChainId: false,
    chainId,
    connector,
  })

  const resolvedChainId = chainId ?? client.chain?.id
  if (!resolvedChainId) throw new Error('`chainId` is required.')

  const account_ = account ?? client.account
  if (!account_) throw new Error('`account` is required.')

  const accountAddress = parseAccount(account_).address
  const {
    amount,
    memo,
    recipient = accountAddress,
    token,
    zoneId,
    ...tx
  } = rest
  const portal = resolvePortal(config, resolvedChainId, zoneId)
  const portalAddress = portal.address
  const tokenAddress = TokenId.toAddress(token)
  const [publicKey, keyIndex] =
    portal.sequencerEncryptionKey && portal.encryptionKeyCount !== undefined
      ? [portal.sequencerEncryptionKey, portal.encryptionKeyCount]
      : await Promise.all([
          viem_readContract(client, {
            address: portalAddress,
            abi: ZoneAbis.zonePortal,
            functionName: 'sequencerEncryptionKey',
          }).then(([x, yParity]) => ({ x, yParity: Number(yParity) })),
          viem_readContract(client, {
            address: portalAddress,
            abi: ZoneAbis.zonePortal,
            functionName: 'encryptionKeyCount',
          }),
        ])
  if (keyIndex === 0n)
    throw new Error('No sequencer encryption key configured.')
  const encrypted = await encryptDepositPayload(publicKey, recipient, memo)

  const receipt = await viem_sendTransactionSync(client, {
    ...tx,
    calls: [
      {
        data: encodeFunctionData({
          abi: Abis.tip20,
          functionName: 'approve',
          args: [portalAddress, amount],
        }),
        to: tokenAddress,
      },
      {
        data: encodeFunctionData({
          abi: ZoneAbis.zonePortal,
          functionName: 'depositEncrypted',
          args: [tokenAddress, amount, keyIndex - 1n, encrypted],
        }),
        to: portalAddress,
      },
    ],
    throwOnReceiptRevert,
  } as never)

  return { receipt }
}

export declare namespace encryptedDepositSync {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.encryptedDepositSync.Parameters<
        config['chains'][number],
        Account
      >,
      'chain'
    >

  export type ReturnValue = Actions.zone.encryptedDepositSync.ReturnValue

  export type ErrorType = Actions.zone.encryptedDepositSync.ErrorType
}

/**
 * Requests a withdrawal from a zone to the parent Tempo chain.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const hash = await Actions.zone.requestWithdrawal(config, {
 *   amount: 1_000_000n,
 *   chainId: zoneChain.id,
 *   token: '0x20c0000000000000000000000000000000000001',
 * })
 *
 * console.log(hash)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns Transaction hash.
 */
export async function requestWithdrawal<config extends Config>(
  config: config,
  parameters: requestWithdrawal.Parameters<config>,
): Promise<requestWithdrawal.ReturnValue> {
  const { account, chainId, connector } = parameters
  const client = await getZoneWalletClient(config, {
    account,
    chainId,
    connector,
  })
  return Actions.zone.requestWithdrawal(client, parameters as never)
}

export declare namespace requestWithdrawal {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.requestWithdrawal.Parameters<
        config['chains'][number],
        Account
      >,
      'chain'
    >

  export type ReturnValue = Actions.zone.requestWithdrawal.ReturnValue

  export type ErrorType = Actions.zone.requestWithdrawal.ErrorType
}

/**
 * Requests a withdrawal from a zone to the parent Tempo chain.
 *
 * Note: This is a synchronous action that waits for the transaction to
 * be included on a block before returning a response.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const result = await Actions.zone.requestWithdrawalSync(config, {
 *   amount: 1_000_000n,
 *   chainId: zoneChain.id,
 *   token: '0x20c0000000000000000000000000000000000001',
 * })
 *
 * console.log(result.receipt.transactionHash)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The transaction receipt.
 */
export async function requestWithdrawalSync<config extends Config>(
  config: config,
  parameters: requestWithdrawalSync.Parameters<config>,
): Promise<requestWithdrawalSync.ReturnValue> {
  const { account, chainId, connector } = parameters
  const client = await getZoneWalletClient(config, {
    account,
    chainId,
    connector,
  })
  return Actions.zone.requestWithdrawalSync(client, parameters as never)
}

export declare namespace requestWithdrawalSync {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.requestWithdrawalSync.Parameters<
        config['chains'][number],
        Account
      >,
      'chain'
    >

  export type ReturnValue = Actions.zone.requestWithdrawalSync.ReturnValue

  export type ErrorType = Actions.zone.requestWithdrawalSync.ErrorType
}

/**
 * Requests a verifiable withdrawal from a zone to the parent Tempo chain.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const hash = await Actions.zone.requestVerifiableWithdrawal(config, {
 *   amount: 1_000_000n,
 *   chainId: zoneChain.id,
 *   revealTo:
 *     '0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
 *   token: '0x20c0000000000000000000000000000000000001',
 * })
 *
 * console.log(hash)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns Transaction hash.
 */
export async function requestVerifiableWithdrawal<config extends Config>(
  config: config,
  parameters: requestVerifiableWithdrawal.Parameters<config>,
): Promise<requestVerifiableWithdrawal.ReturnValue> {
  const { account, chainId, connector } = parameters
  const client = await getZoneWalletClient(config, {
    account,
    chainId,
    connector,
  })
  return Actions.zone.requestVerifiableWithdrawal(client, parameters as never)
}

export declare namespace requestVerifiableWithdrawal {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.requestVerifiableWithdrawal.Parameters<
        config['chains'][number],
        Account
      >,
      'chain'
    >

  export type ReturnValue = Actions.zone.requestVerifiableWithdrawal.ReturnValue

  export type ErrorType = Actions.zone.requestVerifiableWithdrawal.ErrorType
}

/**
 * Requests a verifiable withdrawal from a zone to the parent Tempo chain.
 *
 * Note: This is a synchronous action that waits for the transaction to
 * be included on a block before returning a response.
 *
 * @example
 * ```ts
 * import { createConfig } from '@wagmi/core'
 * import { Actions, dangerous_secp256k1 } from '@wagmi/core/tempo'
 * import { Account } from 'viem/tempo'
 * import { http as zoneHttp, zone } from 'viem/tempo/zones'
 *
 * const zoneChain = zone(7)
 * const account = Account.fromSecp256k1('0x...')
 * const config = createConfig({
 *   chains: [zoneChain],
 *   connectors: [dangerous_secp256k1({ account })],
 *   transports: {
 *     [zoneChain.id]: zoneHttp(),
 *   },
 * })
 *
 * const result = await Actions.zone.requestVerifiableWithdrawalSync(config, {
 *   amount: 1_000_000n,
 *   chainId: zoneChain.id,
 *   revealTo:
 *     '0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
 *   token: '0x20c0000000000000000000000000000000000001',
 * })
 *
 * console.log(result.receipt.transactionHash)
 * ```
 *
 * @param config - Config.
 * @param parameters - Parameters.
 * @returns The transaction receipt.
 */
export async function requestVerifiableWithdrawalSync<config extends Config>(
  config: config,
  parameters: requestVerifiableWithdrawalSync.Parameters<config>,
): Promise<requestVerifiableWithdrawalSync.ReturnValue> {
  const { account, chainId, connector } = parameters
  const client = await getZoneWalletClient(config, {
    account,
    chainId,
    connector,
  })
  return Actions.zone.requestVerifiableWithdrawalSync(
    client,
    parameters as never,
  )
}

export declare namespace requestVerifiableWithdrawalSync {
  export type Parameters<config extends Config> = ChainIdParameter<config> &
    ConnectorParameter &
    UnionLooseOmit<
      Actions.zone.requestVerifiableWithdrawalSync.Parameters<
        config['chains'][number],
        Account
      >,
      'chain'
    >

  export type ReturnValue =
    Actions.zone.requestVerifiableWithdrawalSync.ReturnValue

  export type ErrorType = Actions.zone.requestVerifiableWithdrawalSync.ErrorType
}

const portalAddresses = {
  42431: {
    6: '0x7069DeC4E64Fd07334A0933eDe836C17259c9B23',
    7: '0x3F5296303400B56271b476F5A0B9cBF74350D6Ac',
  },
} as const satisfies Record<number, Record<number, Address>>

async function getZoneWalletClient<config extends Config>(
  config: config,
  parameters: {
    account?: Address | Account | null | undefined
    chainId?: number | undefined
    connector?: ConnectorParameter['connector']
  },
) {
  const client = await getConnectorClient(config, {
    ...parameters,
    assertChainId: false,
  })
  const resolvedChainId = parameters.chainId ?? client.chain?.id
  const account =
    resolveSignableAccount(parameters.account) ??
    (await getSignableConnectorAccount(config, {
      account: parameters.account ?? client.account,
      chainId: resolvedChainId,
      connector: parameters.connector,
    }))
  if (!account || resolvedChainId === undefined) return client

  // Local accounts can sign against the zone transport directly without
  // depending on the connector provider's currently selected chain.
  return Object.assign(config.getClient({ chainId: resolvedChainId }), {
    account,
  }) as typeof client
}

async function getSignableConnectorAccount<config extends Config>(
  config: config,
  parameters: {
    account?: Address | Account | null | undefined
    chainId?: number | undefined
    connector?: ConnectorParameter['connector']
  },
) {
  const connector =
    parameters.connector ??
    config.state.connections.get(config.state.current!)?.connector
  const provider = (await connector?.getProvider?.({
    chainId: parameters.chainId,
  })) as
    | {
        getAccount?:
          | ((parameters?: {
              address?: Address | undefined
              signable?: boolean | undefined
            }) => Account | undefined)
          | undefined
      }
    | undefined
  if (typeof provider?.getAccount !== 'function') return

  try {
    return provider.getAccount({
      address: parameters.account
        ? parseAccount(parameters.account).address
        : undefined,
      signable: true,
    })
  } catch {
    return
  }
}

function resolveSignableAccount(
  account?: Address | Account | null | undefined,
) {
  if (typeof account !== 'object' || account === null) return
  return 'sign' in account ? account : undefined
}

function resolvePortal<config extends Config>(
  config: config,
  chainId: number,
  zoneId: number,
): {
  address: Address
  encryptionKeyCount?: bigint | undefined
  sequencerEncryptionKey?: { x: Hex; yParity: number } | undefined
} {
  const chain = config.chains.find((chain) => chain.id === chainId)
  const zonePortal = chain?.contracts?.zonePortal as
    | Address
    | {
        [key: number]:
          | Address
          | {
              address?: Address | undefined
              encryptionKeyCount?: bigint | undefined
              sequencerEncryptionKey?: { x: Hex; yParity: number } | undefined
            }
          | undefined
        address?: Address | undefined
        encryptionKeyCount?: bigint | undefined
        sequencerEncryptionKey?: { x: Hex; yParity: number } | undefined
      }
    | undefined

  // Allow custom chains to supply portal addresses until viem exposes a
  // generic resolver for non-hardcoded Tempo networks.
  if (typeof zonePortal === 'string') return { address: zonePortal }
  if (zonePortal && typeof zonePortal === 'object') {
    const portal =
      'address' in zonePortal && typeof zonePortal.address === 'string'
        ? zonePortal
        : zonePortal[zoneId]

    if (typeof portal === 'string') return { address: portal as Address }
    if (
      portal &&
      typeof portal === 'object' &&
      typeof portal.address === 'string'
    ) {
      return {
        address: portal.address,
        encryptionKeyCount: portal.encryptionKeyCount,
        sequencerEncryptionKey: portal.sequencerEncryptionKey,
      }
    }
  }

  const address = (portalAddresses as Record<number, Record<number, Address>>)[
    chainId
  ]?.[zoneId]
  if (address) return { address }

  throw new Error(
    `No portal address configured for zone ${zoneId} on chain ${chainId}.`,
  )
}

async function encryptDepositPayload(
  publicKey: { x: Hex; yParity: number },
  recipient: Address,
  memo: Hex = zeroHash,
): Promise<{
  ciphertext: Hex
  ephemeralPubkeyX: Hex
  ephemeralPubkeyYParity: number
  nonce: Hex
  tag: Hex
}> {
  const sequencerPublicKey = PublicKey.from({
    prefix: publicKey.yParity,
    x: BigInt(publicKey.x),
  })

  const { privateKey: ephemeralPrivateKey, publicKey: ephemeralPublicKey } =
    Secp256k1.createKeyPair()

  const sharedSecret = Secp256k1.getSharedSecret({
    privateKey: ephemeralPrivateKey,
    publicKey: sequencerPublicKey,
    as: 'Bytes',
  })

  const hkdfKey = await globalThis.crypto.subtle.importKey(
    'raw',
    sharedSecret.buffer as ArrayBuffer,
    'HKDF',
    false,
    ['deriveKey'],
  )
  const aesKey = await globalThis.crypto.subtle.deriveKey(
    {
      name: 'HKDF',
      hash: 'SHA-256',
      salt: new Uint8Array(12),
      info: new TextEncoder().encode('ecies-aes-key'),
    },
    hkdfKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt'],
  )

  const nonce = Bytes.random(12)
  const plaintext = encodeAbiParameters(
    [{ type: 'address' }, { type: 'bytes32' }],
    [recipient, memo],
  )
  const ciphertextWithTag = new Uint8Array(
    await globalThis.crypto.subtle.encrypt(
      { name: 'AES-GCM', iv: nonce as BufferSource, tagLength: 128 },
      aesKey,
      Bytes.from(plaintext) as BufferSource,
    ),
  )
  const ciphertext = ciphertextWithTag.slice(0, -16)
  const tag = ciphertextWithTag.slice(-16)
  const compressedEphemeral = PublicKey.compress(ephemeralPublicKey)

  return {
    ciphertext: bytesToHex(ciphertext),
    ephemeralPubkeyX: `0x${compressedEphemeral.x.toString(16).padStart(64, '0')}`,
    ephemeralPubkeyYParity: compressedEphemeral.prefix,
    nonce: bytesToHex(nonce),
    tag: bytesToHex(tag),
  }
}

function bytesToHex(bytes: Uint8Array): Hex {
  return `0x${Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('')}` as Hex
}
