import {
  Address,
  BIGINT_0,
  EthereumJSErrorWithoutCode,
  SECP256K1_ORDER_DIV_2,
  bigIntMax,
  bigIntToUnpaddedBytes,
  bytesToHex,
  ecrecover,
  publicToAddress,
  unpadBytes,
} from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { Capability, TransactionType } from '../types.ts'

import { secp256k1 } from 'ethereum-cryptography/secp256k1'
import type { LegacyTx } from '../legacy/tx.ts'
import type { LegacyTxInterface, Transaction } from '../types.ts'

export function errorMsg(tx: LegacyTxInterface, msg: string) {
  return `${msg} (${tx.errorStr()})`
}

export function isSigned(tx: LegacyTxInterface): boolean {
  const { v, r, s } = tx
  if (v === undefined || r === undefined || s === undefined) {
    return false
  } else {
    return true
  }
}

/**
 * The amount of gas paid for the data in this tx
 */
export function getDataGas(tx: LegacyTxInterface): bigint {
  if (tx.cache.dataFee && tx.cache.dataFee.hardfork === tx.common.hardfork()) {
    return tx.cache.dataFee.value
  }

  const txDataZero = tx.common.param('txDataZeroGas')
  const txDataNonZero = tx.common.param('txDataNonZeroGas')

  let cost = BIGINT_0
  for (let i = 0; i < tx.data.length; i++) {
    tx.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero)
  }

  if ((tx.to === undefined || tx.to === null) && tx.common.isActivatedEIP(3860)) {
    const dataLength = BigInt(Math.ceil(tx.data.length / 32))
    const initCodeCost = tx.common.param('initCodeWordGas') * dataLength
    cost += initCodeCost
  }

  if (Object.isFrozen(tx)) {
    tx.cache.dataFee = {
      value: cost,
      hardfork: tx.common.hardfork(),
    }
  }

  return cost
}

/**
 * The minimum gas limit which the tx to have to be valid.
 * This covers costs as the standard fee (21000 gas), the data fee (paid for each calldata byte),
 * the optional creation fee (if the transaction creates a contract), and if relevant the gas
 * to be paid for access lists (EIP-2930) and authority lists (EIP-7702).
 */
export function getIntrinsicGas(tx: LegacyTxInterface): bigint {
  const txFee = tx.common.param('txGas')
  let fee = tx.getDataGas()
  if (txFee) fee += txFee
  if (tx.common.gteHardfork('homestead') && tx.toCreationAddress()) {
    const txCreationFee = tx.common.param('txCreationGas')
    if (txCreationFee) fee += txCreationFee
  }
  return fee
}

export function toCreationAddress(tx: LegacyTxInterface): boolean {
  return tx.to === undefined || tx.to.bytes.length === 0
}

export function hash(tx: LegacyTxInterface): Uint8Array {
  if (!tx.isSigned()) {
    const msg = errorMsg(tx, 'Cannot call hash method if transaction is not signed')
    throw EthereumJSErrorWithoutCode(msg)
  }

  const keccakFunction = tx.common.customCrypto.keccak256 ?? keccak256

  if (Object.isFrozen(tx)) {
    if (!tx.cache.hash) {
      tx.cache.hash = keccakFunction(tx.serialize())
    }
    return tx.cache.hash
  }

  return keccakFunction(tx.serialize())
}

/**
 * EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2are considered invalid.
 * Reasoning: https://ethereum.stackexchange.com/a/55728
 */
export function validateHighS(tx: LegacyTxInterface): void {
  const { s } = tx
  if (tx.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) {
    const msg = errorMsg(
      tx,
      'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid',
    )
    throw EthereumJSErrorWithoutCode(msg)
  }
}

export function getSenderPublicKey(tx: LegacyTxInterface): Uint8Array {
  if (tx.cache.senderPubKey !== undefined) {
    return tx.cache.senderPubKey
  }

  const msgHash = tx.getMessageToVerifySignature()

  const { v, r, s } = tx

  validateHighS(tx)

  try {
    const ecrecoverFunction = tx.common.customCrypto.ecrecover ?? ecrecover
    const sender = ecrecoverFunction(
      msgHash,
      v!,
      bigIntToUnpaddedBytes(r!),
      bigIntToUnpaddedBytes(s!),
      tx.supports(Capability.EIP155ReplayProtection) ? tx.common.chainId() : undefined,
    )
    if (Object.isFrozen(tx)) {
      tx.cache.senderPubKey = sender
    }
    return sender
  } catch {
    const msg = errorMsg(tx, 'Invalid Signature')
    throw EthereumJSErrorWithoutCode(msg)
  }
}

export function getEffectivePriorityFee(gasPrice: bigint, baseFee: bigint | undefined): bigint {
  if (baseFee !== undefined && baseFee > gasPrice) {
    throw EthereumJSErrorWithoutCode('Tx cannot pay baseFee')
  }

  if (baseFee === undefined) {
    return gasPrice
  }

  return gasPrice - baseFee
}

/**
 * Validates the transaction signature and minimum gas requirements.
 * @returns {string[]} an array of error strings
 */
export function getValidationErrors(tx: LegacyTxInterface): string[] {
  const errors = []

  if (tx.isSigned() && !tx.verifySignature()) {
    errors.push('Invalid Signature')
  }

  let intrinsicGas = tx.getIntrinsicGas()
  if (tx.common.isActivatedEIP(7623)) {
    let tokens = 0
    for (let i = 0; i < tx.data.length; i++) {
      tokens += tx.data[i] === 0 ? 1 : 4
    }
    const floorCost =
      tx.common.param('txGas') + tx.common.param('totalCostFloorPerToken') * BigInt(tokens)
    intrinsicGas = bigIntMax(intrinsicGas, floorCost)
  }
  if (intrinsicGas > tx.gasLimit) {
    errors.push(
      `gasLimit is too low. The gasLimit is lower than the minimum gas limit of ${tx.getIntrinsicGas()}, the gas limit is: ${tx.gasLimit}`,
    )
  }

  return errors
}

/**
 * Validates the transaction signature and minimum gas requirements.
 * @returns {boolean} true if the transaction is valid, false otherwise
 */
export function isValid(tx: LegacyTxInterface): boolean {
  const errors = tx.getValidationErrors()

  return errors.length === 0
}

/**
 * Determines if the signature is valid
 */
export function verifySignature(tx: LegacyTxInterface): boolean {
  try {
    // Main signature verification is done in `getSenderPublicKey()`
    const publicKey = tx.getSenderPublicKey()
    return unpadBytes(publicKey).length !== 0
  } catch {
    return false
  }
}

/**
 * Returns the sender's address
 */
export function getSenderAddress(tx: LegacyTxInterface): Address {
  return new Address(publicToAddress(tx.getSenderPublicKey()))
}

/**
 * Signs a transaction.
 *
 * Note that the signed tx is returned as a new object,
 * use as follows:
 * ```javascript
 * const signedTx = tx.sign(privateKey)
 * ```
 */
export function sign(
  tx: LegacyTxInterface,
  privateKey: Uint8Array,
  extraEntropy: Uint8Array | boolean = true,
): Transaction[TransactionType] {
  if (privateKey.length !== 32) {
    // TODO figure out this errorMsg logic how this diverges on other txs
    const msg = errorMsg(tx, 'Private key must be 32 bytes in length.')
    throw EthereumJSErrorWithoutCode(msg)
  }

  // TODO (Jochem, 05 nov 2024): figure out what this hack does and clean it up

  // Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature
  // and want to recreate a signature (where EIP155 should be applied)
  // Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail
  // 2021-06-23
  let hackApplied = false
  if (
    tx.type === TransactionType.Legacy &&
    tx.common.gteHardfork('spuriousDragon') &&
    !tx.supports(Capability.EIP155ReplayProtection)
  ) {
    ;(tx as LegacyTx)['activeCapabilities'].push(Capability.EIP155ReplayProtection)
    hackApplied = true
  }

  const msgHash = tx.getHashedMessageToSign()
  const ecSignFunction = tx.common.customCrypto?.ecsign ?? secp256k1.sign
  const { recovery, r, s } = ecSignFunction(msgHash, privateKey, { extraEntropy })
  const signedTx = tx.addSignature(BigInt(recovery), r, s, true)

  // Hack part 2
  if (hackApplied) {
    const index = (tx as LegacyTx)['activeCapabilities'].indexOf(Capability.EIP155ReplayProtection)
    if (index > -1) {
      ;(tx as LegacyTx)['activeCapabilities'].splice(index, 1)
    }
  }

  return signedTx
}

// TODO maybe move this to shared methods (util.ts in features)
export function getSharedErrorPostfix(tx: LegacyTxInterface) {
  let hash = ''
  try {
    hash = tx.isSigned() ? bytesToHex(tx.hash()) : 'not available (unsigned)'
  } catch {
    hash = 'error'
  }
  let isSigned = ''
  try {
    isSigned = tx.isSigned().toString()
  } catch {
    hash = 'error'
  }
  let hf = ''
  try {
    hf = tx.common.hardfork()
  } catch {
    hf = 'error'
  }

  let postfix = `tx type=${tx.type} hash=${hash} nonce=${tx.nonce} value=${tx.value} `
  postfix += `signed=${isSigned} hf=${hf}`

  return postfix
}
