import { type DecodeErrorResultReturnType, type Hex, decodeErrorResult } from 'viem'
import { Abis } from 'viem/tempo'

import type { OneOf, UnionOmit } from '../internal/types.js'

type AllAbis = typeof Abis.abis
type AbiErrorName = Extract<AllAbis[number], { type: 'error' }>['name']

/** Decoded execution error from a Tempo precompile revert. */
export type ExecutionError = OneOf<
  | (DecodeErrorResultReturnType<AllAbis> & {
      data: Hex
      message: string
    })
  | { errorName: 'unknown'; message: string }
>

/** RPC-serialized execution error (bigints and numbers as hex). */
export type Rpc = UnionOmit<ExecutionError, 'args'>

/** Human-readable messages keyed by ABI error name. */
export const messages: Record<AbiErrorName, string> = {
  AddressAlreadyHasValidator: 'This address already has a validator.',
  AddressNotReserved: 'Address is not reserved.',
  AddressReserved: 'Address is reserved.',
  AlreadyInitialized: 'Already initialized.',
  BelowMinimumOrderSize: 'Order size is below the minimum allowed ({0}).',
  CallNotAllowed: 'This call is not allowed.',
  CannotChangeWithPendingFees: 'Cannot change while fees are pending.',
  CannotChangeWithinBlock: 'Cannot change within the same block.',
  ContractPaused: 'Contract is paused.',
  DivisionByZero: 'Division by zero.',
  EmptyV1ValidatorSet: 'Validator set is empty.',
  ExpiringNonceReplay: 'Expiring nonce has already been used.',
  ExpiringNonceSetFull: 'Expiring nonce set is full.',
  ExpiryInPast: 'Expiry is in the past.',
  IdenticalAddresses: 'Addresses must be different.',
  IdenticalTokens: 'Cannot swap a token for itself — input and output tokens must be different.',
  IncompatiblePolicyType: 'Incompatible policy type.',
  IngressAlreadyExists: 'Ingress "{0}" already exists.',
  InsufficientAllowance: 'Insufficient allowance.',
  InsufficientBalance: 'Insufficient balance. Required: {1}, available: {0}.',
  InsufficientFeeTokenBalance: 'Insufficient fee token balance.',
  InsufficientLiquidity: 'Not enough liquidity in the order book to fill this trade.',
  InsufficientOutput:
    'The output amount is below the slippage minimum — try increasing slippage tolerance.',
  InsufficientReserves: 'Insufficient reserves.',
  InternalError: 'Internal error.',
  InvalidAmount: 'Invalid amount.',
  InvalidBaseToken: 'This token is not a valid base token for the requested pair.',
  InvalidCallScope: 'Invalid call scope.',
  InvalidCurrency: 'Invalid currency.',
  InvalidExpiringNonceExpiry: 'Invalid expiring nonce expiry.',
  InvalidFlipTick: 'The flip-order price tick is invalid.',
  InvalidFormat: 'Invalid format.',
  InvalidMasterAddress: 'Invalid master address.',
  InvalidMigrationIndex: 'Invalid migration index.',
  InvalidNonceKey: 'Invalid nonce key.',
  InvalidOwner: 'Invalid owner.',
  InvalidPayload: 'Invalid payload.',
  InvalidPolicyType: 'Invalid policy type.',
  InvalidPublicKey: 'Invalid public key.',
  InvalidQuoteToken: 'Invalid quote token.',
  InvalidRecipient: 'Invalid recipient.',
  InvalidSignature: 'Invalid signature.',
  InvalidSignatureFormat: 'Invalid signature format.',
  InvalidSignatureType: 'Invalid signature type.',
  InvalidSpendingLimit: 'Invalid spending limit.',
  InvalidSupplyCap: 'Invalid supply cap.',
  InvalidSwapCalculation: 'Invalid swap calculation.',
  InvalidTick: 'The price tick is invalid.',
  InvalidToken: 'This token is not supported on the exchange.',
  InvalidTransferPolicyId: 'Invalid transfer policy.',
  InvalidValidatorAddress: 'Invalid validator address.',
  KeyAlreadyExists: 'Key already exists.',
  KeyAlreadyRevoked: 'Key has already been revoked.',
  KeyExpired: 'Key has expired.',
  KeyNotFound: 'Key not found.',
  LegacyAuthorizeKeySelectorChanged: 'Legacy authorize key selector changed to {0}.',
  MasterIdCollision: 'Master ID collision with {0}.',
  MaxInputExceeded:
    'The required input exceeds the slippage maximum — try increasing slippage tolerance.',
  MigrationNotComplete: 'Migration is not complete.',
  NoOptedInSupply: 'No opted-in supply.',
  NonceOverflow: 'Nonce overflow.',
  NotHostPort: '"{1}" is not a valid host:port for {0}.',
  NotInitialized: 'Not initialized.',
  NotIp: '"{0}" is not a valid IP address.',
  NotIpPort: '"{1}" is not a valid IP:port for {0}.',
  OnlySystemContract: 'Only callable by system contract.',
  OnlyValidator: 'Only callable by a validator.',
  OrderDoesNotExist: 'No order exists with the given ID.',
  OrderNotStale: 'This order is not yet eligible for stale-cleanup.',
  PairAlreadyExists: 'A trading pair for these tokens has already been created.',
  PairDoesNotExist: 'No trading pair exists for these tokens.',
  PermitExpired: 'Permit has expired.',
  PolicyForbids: 'Forbidden by policy.',
  PolicyNotFound: 'Policy not found.',
  PolicyNotSimple: 'Policy is not a simple policy.',
  PoolDoesNotExist: 'Pool does not exist.',
  ProofOfWorkFailed: 'Proof of work failed.',
  ProtectedAddress: 'Address is protected.',
  ProtocolNonceNotSupported: 'Protocol nonce is not supported.',
  PublicKeyAlreadyExists: 'Public key already exists.',
  SignatureTypeMismatch: 'Signature type mismatch. Expected {0}, got {1}.',
  SpendingLimitExceeded: 'Spending limit exceeded.',
  StringTooLong: 'String is too long.',
  SupplyCapExceeded: 'Supply cap exceeded.',
  TickOutOfBounds: 'Price tick {0} is outside the allowed range.',
  TokenAlreadyExists: 'Token {0} already exists.',
  TokenPolicyForbids: 'Forbidden by token policy.',
  TransfersDisabled: 'Transfers are disabled.',
  Unauthorized: 'Unauthorized.',
  UnauthorizedCaller: 'Unauthorized caller.',
  Uninitialized: 'Uninitialized.',
  ValidatorAlreadyDeactivated: 'Validator is already deactivated.',
  ValidatorAlreadyExists: 'Validator already exists.',
  ValidatorNotFound: 'Validator not found.',
  VirtualAddressNotAllowed: 'Virtual address is not allowed.',
  VirtualAddressUnregistered: 'Virtual address is not registered.',
  ZeroPublicKey: 'Public key cannot be zero.',
}

/** Interpolate `{0}`, `{1}`, … placeholders with args. */
function interpolate(template: string, args?: readonly unknown[]): string {
  if (!args) return template
  return template.replace(/\{(\d+)\}/g, (_, i) => {
    const v = args[Number(i)]
    return v === undefined ? `{${i}}` : String(v)
  })
}

/** Parse a viem error into a structured execution error. */
export function parse(error: Error): ExecutionError {
  const raw =
    (error as { details?: string }).details ??
    (error as { shortMessage?: string }).shortMessage ??
    error.message

  const data = extractRevertData(error)
  if (data) {
    try {
      const decoded = decodeErrorResult({ abi: Abis.abis, data })
      const template = messages[decoded.errorName as AbiErrorName]
      return {
        ...decoded,
        data,
        message: template
          ? interpolate(template, decoded.args as readonly unknown[])
          : raw.replace(/^execution reverted:\s*/i, ''),
      } as never
    } catch {}
  }

  // Fallback: extract error name from human-readable revert message.
  const nameMatch = /:\s*(\w+)\(\w+/.exec(raw)
  const errorName = nameMatch?.[1]
  if (errorName && errorName in messages)
    return { errorName: 'unknown', message: messages[errorName as AbiErrorName]! }

  return {
    errorName: 'unknown',
    message: raw.replace(/^execution reverted:\s*/i, ''),
  }
}

/** Serializes an ExecutionError for RPC transport (bigints/numbers → hex). */
export function serialize(preimage: ExecutionError): Rpc {
  if (preimage.errorName === 'unknown') return { errorName: 'unknown', message: preimage.message }
  return {
    errorName: preimage.errorName,
    abiItem: preimage.abiItem,
    message: preimage.message,
    data: preimage.data,
  } as never
}

function extractRevertData(error: unknown): Hex | null {
  if (!error || typeof error !== 'object') return null
  const e = error as Record<string, unknown>
  if (typeof e.data === 'string' && e.data.startsWith('0x')) return e.data as Hex
  if (e.cause) return extractRevertData(e.cause)
  if (e.error) return extractRevertData(e.error)
  if (typeof e.walk === 'function') {
    const inner = (e as { walk: (fn: (e: unknown) => boolean) => unknown }).walk(
      (e) => typeof (e as Record<string, unknown>).data === 'string',
    )
    if (inner) return extractRevertData(inner)
  }
  return null
}
