import { RpcResponse, Signature } from 'ox'
import { TxEnvelopeTempo } from 'ox/tempo'
import type { Address, Client } from 'viem'
import type { LocalAccount } from 'viem/accounts'
import { Transaction } from 'viem/tempo'

import * as Utils from './utils.js'

/** Returns sponsor metadata for `eth_fillTransaction` responses. */
export function getSponsor(options: getSponsor.Options): getSponsor.ReturnType {
  const { account, name, url } = options
  return {
    address: account.address,
    ...(name ? { name } : {}),
    ...(url ? { url } : {}),
  }
}

export declare namespace getSponsor {
  type Options = {
    /** Account used for sponsorship. */
    account: LocalAccount
    /** Optional display name. */
    name?: string | undefined
    /** Optional display URL. */
    url?: string | undefined
  }

  type ReturnType = {
    /** Sponsor address. */
    address: Address
    /** Sponsor display name. */
    name?: string | undefined
    /** Sponsor display URL. */
    url?: string | undefined
  }
}

/** Returns whether the fee payer approves a filled transaction. */
export async function shouldSponsor(options: shouldSponsor.Options) {
  const { sender, transaction, validate } = options
  if (!validate) return true
  return await validate({
    ...transaction,
    from: sender,
  } as Transaction.TransactionRequest)
}

export declare namespace shouldSponsor {
  type Options = {
    /** Sender address from the original request. */
    sender?: Address | undefined
    /** Filled transaction to validate. */
    transaction: Record<string, unknown>
    /** Optional sponsorship approval callback. */
    validate?: ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>) | undefined
  }
}

/** Returns whether a raw Tempo transaction is explicitly requesting sponsorship. */
export function requestsRawSponsorship(serialized: `0x${string}`) {
  if (!Utils.isSerializedTempoTransaction(serialized)) return false
  const transaction = Transaction.deserialize(serialized)
  return 'feePayerSignature' in transaction && transaction.feePayerSignature === null
}

/** Returns `true` when a fill request already has the fields needed for sponsorship signing. */
export function isPreparedTransaction(value: Record<string, unknown>) {
  return (
    typeof value.from === 'string' &&
    typeof Utils.resolveChainId(value.chainId) === 'number' &&
    typeof value.gas !== 'undefined' &&
    typeof value.nonce !== 'undefined' &&
    (typeof value.maxFeePerGas !== 'undefined' || typeof value.gasPrice !== 'undefined')
  )
}

/** Signs a filled transaction as the fee payer. */
export async function sign(options: sign.Options) {
  const { account, transaction, sender } = options
  const from = (transaction.from as Address | undefined) ?? sender
  const { signature: _, ...withoutSenderSig } = transaction
  const prepared = { ...withoutSenderSig, from }

  if (!prepared.from)
    throw new RpcResponse.InvalidParamsError({
      message: 'Transaction sender must be provided before fee payer signing.',
    })
  if (!account.sign) throw new Error('Fee payer account cannot sign transactions.')

  const feePayerSignature = Signature.from(
    await account.sign({
      hash: TxEnvelopeTempo.getFeePayerSignPayload(TxEnvelopeTempo.from(prepared as never), {
        sender: prepared.from,
      }),
    }),
  )

  return { ...prepared, feePayerSignature }
}

export declare namespace sign {
  type Options = {
    /** Account used as the fee payer. */
    account: LocalAccount
    /** Filled transaction to sign. */
    transaction: Record<string, unknown>
    /** Sender address from the original request. */
    sender?: Address | undefined
  }
}

/** Handles `eth_signRawTransaction` and broadcast methods for sponsored Tempo transactions. */
export async function handleRawTransaction(options: handleRawTransaction.Options) {
  const { account, feeToken: sponsorFeeToken, getClient, method, request, validate } = options
  const serialized = request.params?.[0] as `0x76${string}` | undefined

  if (!Utils.isSerializedTempoTransaction(serialized))
    throw new RpcResponse.InvalidParamsError({
      message: 'Only Tempo (0x76/0x78) transactions are supported.',
    })

  const transaction = Transaction.deserialize(serialized)
  // Prefer sender recovered from raw envelope; multisig finalize path supplies fallback sender.
  const sender = transaction.from ?? options.sender

  // Sponsorship only applies after sender has signed original transaction.
  if (!transaction.signature || !sender)
    throw new RpcResponse.InvalidParamsError({
      message: 'Transaction must be signed by the sender before fee payer signing.',
    })
  if (!account.sign) throw new Error('Fee payer account cannot sign transactions.')

  const client = getClient(transaction.chainId)
  const feeToken_chain = (client.chain as { feeToken?: Address | undefined } | undefined)?.feeToken
  const feeToken =
    (transaction.feeToken as Address | null | undefined) ??
    sponsorFeeToken ??
    (await options.getFeeToken?.(transaction.chainId)) ??
    feeToken_chain
  const transaction_sponsored = feeToken ? { ...transaction, feeToken } : transaction

  if (validate && !(await validate(transaction_sponsored as Transaction.TransactionRequest)))
    throw new RpcResponse.InvalidParamsError({
      message: 'Sponsorship rejected.',
    })

  const envelope = TxEnvelopeTempo.from(transaction_sponsored as never)
  const feePayerSignature = Signature.from(
    await account.sign({
      hash: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }),
    }),
  )
  const serializedTransaction = TxEnvelopeTempo.serialize(envelope, {
    feePayerSignature,
    signature: transaction.signature,
  })

  // Raw-sign requests stop after fee-payer signature is added; send methods broadcast it.
  if (method === 'eth_signRawTransaction') return serializedTransaction
  return await client.request({
    method: method as never,
    params: [serializedTransaction],
  })
}

export declare namespace handleRawTransaction {
  type Options = {
    /** Account used as the fee payer. */
    account: LocalAccount
    /** Optional token the fee payer prefers for sponsored raw transactions. */
    feeToken?: Address | undefined
    /** Optional fee-token resolver used when the raw envelope omits `feeToken`. */
    getFeeToken?: ((chainId: number) => Promise<Address | undefined>) | undefined
    /** Client resolver keyed by transaction `chainId`. */
    getClient: (chainId?: number | undefined) => Client
    /** Raw transaction method to handle. */
    method: 'eth_signRawTransaction' | 'eth_sendRawTransaction' | 'eth_sendRawTransactionSync'
    /** Incoming JSON-RPC request. */
    request: { params?: readonly unknown[] | undefined }
    /** Sender address to use if it cannot be recovered from the raw envelope. */
    sender?: Address | undefined
    /** Optional sponsorship approval callback. */
    validate?: ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>) | undefined
  }
}
