// TODO: Find opportunities to make this file less duplicated + more simplified with Viem v3.

import type { Address } from 'abitype'
import * as Hex from 'ox/Hex'
import * as Secp256k1 from 'ox/Secp256k1'
import * as Signature from 'ox/Signature'
import {
  type AuthorizationTempo,
  type KeyAuthorization,
  type TransactionReceipt as ox_TransactionReceipt,
  SignatureEnvelope,
  type TempoAddress,
  TxEnvelopeTempo as TxTempo,
} from 'ox/tempo'
import type { Account } from '../accounts/types.js'
import type { ExtractCapabilities } from '../types/capabilities.js'
import type { FeeValuesEIP1559 } from '../types/fee.js'
import type { Signature as viem_Signature } from '../types/misc.js'
import type {
  RpcTransaction as viem_RpcTransaction,
  RpcTransactionRequest as viem_RpcTransactionRequest,
} from '../types/rpc.js'
import type {
  AccessList,
  TransactionBase,
  TransactionRequestBase,
  TransactionSerializableBase,
  TransactionSerializedGeneric,
  Transaction as viem_Transaction,
  TransactionReceipt as viem_TransactionReceipt,
  TransactionRequest as viem_TransactionRequest,
  TransactionSerializable as viem_TransactionSerializable,
  TransactionSerialized as viem_TransactionSerialized,
  TransactionType as viem_TransactionType,
} from '../types/transaction.js'
import type { ExactPartial, OneOf, PartialBy } from '../types/utils.js'
import { getTransactionType as viem_getTransactionType } from '../utils/transaction/getTransactionType.js'
import {
  type ParseTransactionReturnType,
  parseTransaction as viem_parseTransaction,
} from '../utils/transaction/parseTransaction.js'
import { serializeTransaction as viem_serializeTransaction } from '../utils/transaction/serializeTransaction.js'

export type Transaction<
  bigintType = bigint,
  numberType = number,
  pending extends boolean = false,
> = OneOf<
  | viem_Transaction<bigintType, numberType, pending>
  | TransactionTempo<bigintType, numberType, pending>
>
export type TransactionRpc<pending extends boolean = false> = OneOf<
  | viem_RpcTransaction<pending>
  | (Omit<
      TransactionTempo<Hex.Hex, Hex.Hex, pending, '0x76'>,
      'authorizationList' | 'keyAuthorization' | 'signature'
    > & {
      authorizationList?: AuthorizationTempo.ListRpc | undefined
      keyAuthorization?: KeyAuthorization.Rpc | null | undefined
      signature: SignatureEnvelope.SignatureEnvelopeRpc
    })
>

export type TransactionTempo<
  quantity = bigint,
  index = number,
  isPending extends boolean = boolean,
  type = 'tempo',
> = PartialBy<
  Omit<TransactionBase<quantity, index, isPending>, 'input' | 'value' | 'to'>,
  'r' | 's' | 'v' | 'yParity'
> & {
  accessList: AccessList
  authorizationList?: AuthorizationTempo.ListSigned<quantity, index> | undefined
  calls: readonly TxTempo.Call<quantity>[]
  chainId: index
  feeToken?: Address | undefined
  feePayerSignature?: viem_Signature | undefined
  keyAuthorization?: KeyAuthorization.Signed<quantity, index> | null | undefined
  nonceKey?: quantity | undefined
  signature: SignatureEnvelope.SignatureEnvelope
  type: type
  validBefore?: index | undefined
  validAfter?: index | undefined
} & FeeValuesEIP1559<quantity>

export type TransactionRequest<
  bigintType = bigint,
  numberType = number,
> = OneOf<
  | viem_TransactionRequest<bigintType, numberType>
  | TransactionRequestTempo<bigintType, numberType>
>
export type TransactionRequestRpc = OneOf<
  viem_RpcTransactionRequest | TransactionRequestTempo<Hex.Hex, Hex.Hex, '0x76'>
>

export type TransactionReceipt<
  quantity = bigint,
  index = number,
  status = 'success' | 'reverted',
  type = TransactionType,
> = viem_TransactionReceipt<quantity, index, status, type> & {
  feePayer?: Address | undefined
  feeToken?: Address | undefined
}

export type TransactionReceiptRpc = TransactionReceipt<
  Hex.Hex,
  Hex.Hex,
  ox_TransactionReceipt.RpcStatus,
  ox_TransactionReceipt.RpcType
>

export type TransactionRequestTempo<
  quantity = bigint,
  index = number,
  type = 'tempo',
> = TransactionRequestBase<quantity, index, type> &
  ExactPartial<FeeValuesEIP1559<quantity>> & {
    accessList?: AccessList | undefined
    calls?: readonly TxTempo.Call<quantity, TempoAddress.Address>[] | undefined
    capabilities?: ExtractCapabilities<'fillTransaction', 'Request'> | undefined
    feePayer?: Account | true | undefined
    feeToken?: TempoAddress.Address | bigint | undefined
    keyAuthorization?: KeyAuthorization.Signed<quantity, index> | undefined
    nonceKey?: 'expiring' | quantity | undefined
    validBefore?: index | undefined
    validAfter?: index | undefined
  }

export type TransactionSerializable = OneOf<
  viem_TransactionSerializable | TransactionSerializableTempo
>

export type TransactionSerializableTempo<
  quantity = bigint,
  index = number,
> = TransactionSerializableBase<quantity, index> &
  ExactPartial<FeeValuesEIP1559<quantity>> & {
    accessList?: AccessList | undefined
    calls: readonly TxTempo.Call<quantity>[]
    chainId: number
    feeToken?: Address | bigint | undefined
    feePayerSignature?: viem_Signature | null | undefined
    from?: Address | undefined
    keyAuthorization?: KeyAuthorization.Signed<quantity, index> | undefined
    nonceKey?: quantity | undefined
    signature?: SignatureEnvelope.SignatureEnvelope<quantity, index> | undefined
    validBefore?: index | undefined
    validAfter?: index | undefined
    type?: 'tempo' | undefined
  }

export type TransactionSerialized<
  type extends TransactionType = TransactionType,
> = viem_TransactionSerialized<type> | TransactionSerializedTempo

export type TransactionSerializedTempo = `0x76${string}`
export type TransactionSerializedFeePayer = `0x78${string}`

export type TransactionType = viem_TransactionType | 'tempo'

export function getType(
  transaction: Record<string, unknown>,
): Transaction['type'] {
  const account = transaction.account as
    | { keyType?: string | undefined; source?: string | undefined }
    | undefined
  if (
    (account?.keyType && account.keyType !== 'secp256k1') ||
    account?.source === 'accessKey' ||
    typeof transaction.calls !== 'undefined' ||
    typeof transaction.feePayer !== 'undefined' ||
    typeof transaction.feeToken !== 'undefined' ||
    typeof transaction.keyAuthorization !== 'undefined' ||
    typeof transaction.nonceKey !== 'undefined' ||
    typeof transaction.signature !== 'undefined' ||
    typeof transaction.validBefore !== 'undefined' ||
    typeof transaction.validAfter !== 'undefined'
  )
    return 'tempo' as never
  if (transaction.type) return transaction.type as never
  return viem_getTransactionType(transaction) as never
}

export function isTempo(transaction: Record<string, unknown>) {
  try {
    const type = getType(transaction)
    return type === 'tempo'
  } catch {
    return false
  }
}

export function deserialize<
  const serialized extends TransactionSerializedGeneric,
>(serializedTransaction: serialized): deserialize.ReturnValue<serialized> {
  const type = Hex.slice(serializedTransaction, 0, 1)
  if (type === '0x76' || type === '0x78')
    return deserializeTempo(serializedTransaction as `0x76${string}`) as never
  return viem_parseTransaction(serializedTransaction) as never
}

export declare namespace deserialize {
  export type ReturnValue<
    serialized extends
      TransactionSerializedGeneric = TransactionSerializedGeneric,
  > = serialized extends TransactionSerializedTempo
    ? TransactionSerializableTempo
    : serialized extends TransactionSerializedFeePayer
      ? TransactionSerializableTempo
      : ParseTransactionReturnType<serialized>
}

export async function serialize(
  transaction: TransactionSerializable & {
    feePayer?: Account | true | undefined
    from?: TempoAddress.Address | undefined
  },
  signature?:
    | OneOf<SignatureEnvelope.SignatureEnvelope | viem_Signature>
    | undefined,
) {
  // If the transaction is not a Tempo transaction, route to Viem serializer.
  if (!isTempo(transaction)) {
    if (signature && 'type' in signature && signature.type !== 'secp256k1')
      throw new Error(
        'Unsupported signature type. Expected `secp256k1` but got `' +
          signature.type +
          '`.',
      )
    if (signature && 'type' in signature) {
      const { r, s, yParity } = signature?.signature!
      return viem_serializeTransaction(transaction as never, {
        r: Hex.fromNumber(r, { size: 32 }),
        s: Hex.fromNumber(s, { size: 32 }),
        yParity,
      })
    }
    return viem_serializeTransaction(transaction as never, signature)
  }

  const type = getType(transaction)
  if (type === 'tempo') return serializeTempo(transaction as never, signature)

  throw new Error('Unsupported transaction type')
}

////////////////////////////////////////////////////////////////////////////////////
// Internal

/** @internal */
function deserializeTempo(
  serializedTransaction: TransactionSerializedTempo,
): TransactionSerializableTempo {
  const { feePayerSignature, nonce, ...tx } = TxTempo.deserialize(
    serializedTransaction,
  )
  return {
    ...tx,
    nonce: Number(nonce ?? 0n),
    feePayerSignature: feePayerSignature
      ? {
          r: Hex.fromNumber(feePayerSignature.r, { size: 32 }),
          s: Hex.fromNumber(feePayerSignature.s, { size: 32 }),
          yParity: feePayerSignature.yParity,
        }
      : feePayerSignature,
  } satisfies TransactionSerializableTempo
}

/** @internal */
async function serializeTempo(
  transaction: TransactionSerializableTempo & {
    feePayer?: Account | true | undefined
    from?: Address | undefined
  },
  sig?: OneOf<SignatureEnvelope.SignatureEnvelope | viem_Signature> | undefined,
) {
  const signature = (() => {
    if (transaction.signature) return transaction.signature
    if (sig && 'type' in sig) return sig as SignatureEnvelope.SignatureEnvelope
    if (sig)
      return SignatureEnvelope.from({
        r: BigInt(sig.r!),
        s: BigInt(sig.s!),
        yParity: Number(sig.yParity!),
      })
    return undefined
  })()

  const { chainId, feePayer, nonce, ...rest } = transaction

  const feePayerSignature = (() => {
    const feePayerSignature = transaction.feePayerSignature
    if (feePayerSignature)
      return {
        r: BigInt(feePayerSignature.r!),
        s: BigInt(feePayerSignature.s!),
        yParity: Number(feePayerSignature.yParity),
      }
    if (feePayerSignature === null || feePayer) return null
    return undefined
  })()
  const hasPrefilledFeePayerSignature =
    typeof transaction.feePayerSignature !== 'undefined' &&
    transaction.feePayerSignature !== null
  const shouldStripFeeTokenForSponsorship =
    (feePayer === true && (!signature || !feePayerSignature)) ||
    (!signature && hasPrefilledFeePayerSignature)

  const transaction_ox = {
    ...rest,
    calls: rest.calls?.length
      ? rest.calls
      : [
          {
            to:
              rest.to ||
              (!rest.data || rest.data === '0x'
                ? '0x0000000000000000000000000000000000000000'
                : undefined),
            value: rest.value,
            data: rest.data,
          },
        ],
    chainId: Number(chainId),
    feePayerSignature,
    type: 'tempo',
    ...(nonce ? { nonce: BigInt(nonce) } : {}),
  } satisfies TxTempo.TxEnvelopeTempo

  // Sender does not commit to `feeToken` under sponsorship. Strip it
  // for the sender sign payload and the partial sponsorship handoff envelope.
  // Keep it only on the final broadcast envelope so the chain can verify
  // the fee payer.
  if (shouldStripFeeTokenForSponsorship) delete transaction_ox.feeToken

  if (signature && typeof transaction.feePayer === 'object') {
    const tx = TxTempo.from(transaction_ox, {
      signature,
    })

    const sender = (() => {
      if (transaction.from) return transaction.from
      if (signature.type === 'secp256k1')
        return Secp256k1.recoverAddress({
          payload: TxTempo.getSignPayload(tx),
          signature: signature.signature,
        })
      throw new Error('Unable to extract sender from transaction or signature.')
    })()

    const hash = TxTempo.getFeePayerSignPayload(tx, {
      sender,
    })

    const feePayerSignature = await transaction.feePayer.sign!({
      hash,
    })

    return TxTempo.serialize(tx, {
      feePayerSignature: Signature.from(feePayerSignature),
    })
  }

  if (feePayer === true || (!signature && hasPrefilledFeePayerSignature)) {
    // Fee payer signature was prefilled during `eth_fillTransaction` -- emit
    // a full envelope with both signatures to skip `eth_signRawTransaction`.
    if (signature && feePayerSignature)
      return TxTempo.serialize(transaction_ox, {
        signature,
      })
    if (signature)
      return TxTempo.serialize(transaction_ox, {
        format: 'feePayer',
        sender: transaction.from,
        signature,
      })
    return TxTempo.serialize(transaction_ox, {
      feePayerSignature: null,
    })
  }

  return TxTempo.serialize(
    // If we have specified a fee payer, the user will not be signing over the fee token.
    // Defer the fee token signing to the fee payer. Once the fee payer has signed,
    // keep `feeToken` so the broadcast envelope carries the token the chain must charge.
    {
      ...transaction_ox,
      ...(feePayer && !feePayerSignature ? { feeToken: undefined } : {}),
    },
    {
      feePayerSignature: undefined,
      signature,
    },
  )
}

// Export types required for inference.
// biome-ignore lint/performance/noBarrelFile: _
export {
  /** @deprecated */
  KeyAuthorization as z_KeyAuthorization,
  /** @deprecated */
  SignatureEnvelope as z_SignatureEnvelope,
  /** @deprecated */
  TxEnvelopeTempo as z_TxEnvelopeTempo,
} from 'ox/tempo'
