import type { Address } from 'abitype'
import { SignatureErc6492 } from 'ox/erc6492'
import { SignatureErc8010 } from 'ox/erc8010'

import type { Client } from '../../clients/createClient.js'
import type { Transport } from '../../clients/transports/createTransport.js'
import {
  erc1271Abi,
  erc6492SignatureValidatorAbi,
  multicall3Abi,
} from '../../constants/abis.js'
import {
  erc6492SignatureValidatorByteCode,
  multicall3Bytecode,
} from '../../constants/contracts.js'
import {
  CallExecutionError,
  ContractFunctionExecutionError,
} from '../../errors/contract.js'
import type { InvalidHexBooleanError } from '../../errors/encoding.js'
import type { ErrorType } from '../../errors/utils.js'
import type { Chain } from '../../types/chain.js'
import type { ByteArray, Hex, Signature } from '../../types/misc.js'
import type { OneOf } from '../../types/utils.js'
import {
  type EncodeDeployDataErrorType,
  encodeDeployData,
} from '../../utils/abi/encodeDeployData.js'
import {
  type EncodeFunctionDataErrorType,
  encodeFunctionData,
} from '../../utils/abi/encodeFunctionData.js'
import {
  type GetAddressErrorType,
  getAddress,
} from '../../utils/address/getAddress.js'
import {
  type IsAddressEqualErrorType,
  isAddressEqual,
} from '../../utils/address/isAddressEqual.js'
import { verifyAuthorization } from '../../utils/authorization/verifyAuthorization.js'
import { type ConcatHexErrorType, concatHex } from '../../utils/data/concat.js'
import { type IsHexErrorType, isHex } from '../../utils/data/isHex.js'
import { hexToBool } from '../../utils/encoding/fromHex.js'
import {
  type BytesToHexErrorType,
  bytesToHex,
  type NumberToHexErrorType,
  numberToHex,
} from '../../utils/encoding/toHex.js'
import { getAction } from '../../utils/getAction.js'
import {
  type RecoverAddressErrorType,
  recoverAddress,
} from '../../utils/signature/recoverAddress.js'
import {
  type SerializeSignatureErrorType,
  serializeSignature,
} from '../../utils/signature/serializeSignature.js'
import { type CallErrorType, type CallParameters, call } from './call.js'
import { type GetCodeErrorType, getCode } from './getCode.js'
import { type ReadContractErrorType, readContract } from './readContract.js'

export type VerifyHashParameters = Pick<
  CallParameters,
  'blockNumber' | 'blockTag'
> & {
  /** The address that signed the original message. */
  address: Address
  /** The chain to use. */
  chain?: Chain | null | undefined
  /** The address of the ERC-6492 signature verifier contract. */
  erc6492VerifierAddress?: Address | undefined
  /** The hash to be verified. */
  hash: Hex
  /** Multicall3 address for ERC-8010 verification. */
  multicallAddress?: Address | undefined
  /** The signature that was generated by signing the message with the address's private key. */
  signature: Hex | ByteArray | Signature
  /** @deprecated use `erc6492VerifierAddress` instead. */
  universalSignatureVerifierAddress?: Address | undefined
  /** Chooses which verification path to try first before falling back. */
  mode?: 'auto' | 'eoa' | (string & {}) | undefined
} & OneOf<{ factory: Address; factoryData: Hex } | {}>

export type VerifyHashReturnType = boolean

export type VerifyHashErrorType =
  | BytesToHexErrorType
  | CallErrorType
  | ConcatHexErrorType
  | EncodeDeployDataErrorType
  | EncodeFunctionDataErrorType
  | ErrorType
  | GetAddressErrorType
  | GetCodeErrorType
  | InvalidHexBooleanError
  | IsAddressEqualErrorType
  | IsHexErrorType
  | NumberToHexErrorType
  | ReadContractErrorType
  | RecoverAddressErrorType
  | SerializeSignatureErrorType

/**
 * Verifies a message hash onchain using ERC-6492.
 *
 * @param client - Client to use.
 * @param parameters - {@link VerifyHashParameters}
 * @returns Whether or not the signature is valid. {@link VerifyHashReturnType}
 */
export async function verifyHash<chain extends Chain | undefined>(
  client: Client<Transport, chain>,
  parameters: VerifyHashParameters,
): Promise<VerifyHashReturnType> {
  const {
    address,
    chain = client.chain,
    hash,
    erc6492VerifierAddress:
      verifierAddress = parameters.universalSignatureVerifierAddress ??
      chain?.contracts?.erc6492Verifier?.address,
    multicallAddress = parameters.multicallAddress ??
      chain?.contracts?.multicall3?.address,
    mode = 'auto',
  } = parameters

  if (chain?.verifyHash) return await chain.verifyHash(client, parameters)

  const signature = (() => {
    const signature = parameters.signature
    if (isHex(signature)) return signature
    if (typeof signature === 'object' && 'r' in signature && 's' in signature)
      return serializeSignature(signature)
    return bytesToHex(signature)
  })()
  try {
    if (mode === 'eoa') {
      try {
        const verified = isAddressEqual(
          getAddress(address),
          await recoverAddress({ hash, signature }),
        )
        if (verified) return true
      } catch {}
    }

    if (SignatureErc8010.validate(signature))
      return await verifyErc8010(client, {
        ...parameters,
        multicallAddress,
        signature,
      })
    return await verifyErc6492(client, {
      ...parameters,
      verifierAddress,
      signature,
    })
  } catch (error) {
    if (mode !== 'eoa') {
      // Fallback attempt to verify the signature via ECDSA recovery.
      try {
        const verified = isAddressEqual(
          getAddress(address),
          await recoverAddress({ hash, signature }),
        )
        if (verified) return true
      } catch {}
    }

    if (error instanceof VerificationError) {
      // if the execution fails, the signature was not valid and an internal method inside of the validator reverted
      // this can happen for many reasons, for example if signer can not be recovered from the signature
      // or if the signature has no valid format
      return false
    }

    throw error
  }
}

/** @internal */
export async function verifyErc8010(
  client: Client,
  parameters: verifyErc8010.Parameters,
) {
  const { address, blockNumber, blockTag, hash, multicallAddress } = parameters

  const {
    authorization: authorization_ox,
    data: initData,
    signature,
    to,
  } = SignatureErc8010.unwrap(parameters.signature)

  // Check if already delegated
  const code = await getCode(client, {
    address,
    blockNumber,
    blockTag,
  } as never)

  // If already delegated, perform standard ERC-1271 verification.
  if (code === concatHex(['0xef0100', authorization_ox.address]))
    return await verifyErc1271(client, {
      address,
      blockNumber,
      blockTag,
      hash,
      signature,
    })

  const authorization = {
    address: authorization_ox.address,
    chainId: Number(authorization_ox.chainId),
    nonce: Number(authorization_ox.nonce),
    r: numberToHex(authorization_ox.r, { size: 32 }),
    s: numberToHex(authorization_ox.s, { size: 32 }),
    yParity: authorization_ox.yParity,
  } as const

  const valid = await verifyAuthorization({
    address,
    authorization,
  })
  if (!valid) throw new VerificationError()

  // Deployless verification.
  const results = await getAction(
    client,
    readContract,
    'readContract',
  )({
    ...(multicallAddress
      ? { address: multicallAddress }
      : { code: multicall3Bytecode }),
    authorizationList: [authorization],
    abi: multicall3Abi,
    blockNumber,
    blockTag: 'pending',
    functionName: 'aggregate3',
    args: [
      [
        ...(initData
          ? ([
              {
                allowFailure: true,
                target: to ?? address,
                callData: initData,
              },
            ] as const)
          : []),
        {
          allowFailure: true,
          target: address,
          callData: encodeFunctionData({
            abi: erc1271Abi,
            functionName: 'isValidSignature',
            args: [hash, signature],
          }),
        },
      ],
    ],
  })

  const data = results[results.length - 1]?.returnData

  if (data?.startsWith('0x1626ba7e')) return true
  throw new VerificationError()
}

export namespace verifyErc8010 {
  export type Parameters = Pick<CallParameters, 'blockNumber' | 'blockTag'> & {
    /** The address that signed the original message. */
    address: Address
    /** The hash to be verified. */
    hash: Hex
    /** Multicall3 address for ERC-8010 verification. */
    multicallAddress?: Address | undefined
    /** The signature that was generated by signing the message with the address's private key. */
    signature: Hex
  }
}

/** @internal */
// biome-ignore lint/correctness/noUnusedVariables: _
async function verifyErc6492(
  client: Client,
  parameters: verifyErc6492.Parameters,
) {
  const {
    address,
    factory,
    factoryData,
    hash,
    signature,
    verifierAddress,
    ...rest
  } = parameters

  const wrappedSignature = await (async () => {
    // If no `factory` or `factoryData` is provided, it is assumed that the
    // address is not a Smart Account, or the Smart Account is already deployed.
    if (!factory && !factoryData) return signature

    // If the signature is already wrapped, return the signature.
    if (SignatureErc6492.validate(signature)) return signature

    // If the Smart Account is not deployed, wrap the signature with a 6492 wrapper
    // to perform counterfactual validation.
    return SignatureErc6492.wrap({
      data: factoryData!,
      signature,
      to: factory!,
    })
  })()

  const args = verifierAddress
    ? ({
        to: verifierAddress,
        data: encodeFunctionData({
          abi: erc6492SignatureValidatorAbi,
          functionName: 'isValidSig',
          args: [address, hash, wrappedSignature],
        }),
        ...rest,
      } as unknown as CallParameters)
    : ({
        data: encodeDeployData({
          abi: erc6492SignatureValidatorAbi,
          args: [address, hash, wrappedSignature],
          bytecode: erc6492SignatureValidatorByteCode,
        }),
        ...rest,
      } as unknown as CallParameters)

  const { data } = await getAction(
    client,
    call,
    'call',
  )(args).catch((error) => {
    if (error instanceof CallExecutionError) throw new VerificationError()
    throw error
  })

  if (hexToBool(data ?? '0x0')) return true
  throw new VerificationError()
}

export namespace verifyErc6492 {
  export type Parameters = Pick<CallParameters, 'blockNumber' | 'blockTag'> & {
    /** The address that signed the original message. */
    address: Address
    /** The hash to be verified. */
    hash: Hex
    /** The signature that was generated by signing the message with the address's private key. */
    signature: Hex
    /** The address of the ERC-6492 signature verifier contract. */
    verifierAddress?: Address | undefined
  } & OneOf<{ factory: Address; factoryData: Hex } | {}>
}

/** @internal */
export async function verifyErc1271(
  client: Client,
  parameters: verifyErc1271.Parameters,
) {
  const { address, blockNumber, blockTag, hash, signature } = parameters

  const result = await getAction(
    client,
    readContract,
    'readContract',
  )({
    address,
    abi: erc1271Abi,
    args: [hash, signature],
    blockNumber,
    blockTag,
    functionName: 'isValidSignature',
  }).catch((error) => {
    if (error instanceof ContractFunctionExecutionError)
      throw new VerificationError()
    throw error
  })

  if (result.startsWith('0x1626ba7e')) return true
  throw new VerificationError()
}

export namespace verifyErc1271 {
  export type Parameters = Pick<CallParameters, 'blockNumber' | 'blockTag'> & {
    /** The address that signed the original message. */
    address: Address
    /** The hash to be verified. */
    hash: Hex
    /** The signature that was generated by signing the message with the address's private key. */
    signature: Hex
  }
}

class VerificationError extends Error {}
