import * as Address from 'ox/Address'
import * as Hash from 'ox/Hash'
import * as Hex from 'ox/Hex'
import * as Provider from 'ox/Provider'
import * as RpcRequest from 'ox/RpcRequest'
import type { LocalAccount } from '../accounts/types.js'
import { getTransactionReceipt } from '../actions/public/getTransactionReceipt.js'
import { sendTransaction } from '../actions/wallet/sendTransaction.js'
import { sendTransactionSync } from '../actions/wallet/sendTransactionSync.js'
import { createClient } from '../clients/createClient.js'
import {
  createTransport,
  type Transport,
} from '../clients/transports/createTransport.js'
import type { Chain } from '../types/chain.js'
import type { ChainConfig } from './chainConfig.js'
import * as Transaction from './Transaction.js'

type RelayProxyParameters = {
  /** Policy for how the relay should handle sponsored transactions. Defaults to `'sign-only'`. */
  policy?: 'sign-only' | 'sign-and-broadcast' | undefined
}

export type FeePayer = Transport<typeof withFeePayer.type>
export type Relay = Transport<typeof withRelay.type>

/**
 * Creates a relay transport that routes requests between
 * the default transport or the relay transport.
 *
 * All `eth_fillTransaction` requests are sent to the relay with the request's
 * `feePayer` value preserved so the relay can decide whether to sponsor the transaction.
 *
 * The policy parameter controls how the relay handles sponsored transactions:
 * - `'sign-only'`: Relay co-signs the transaction and returns it to the client transport, which then broadcasts it via the default transport
 * - `'sign-and-broadcast'`: Relay co-signs and broadcasts the transaction directly
 *
 * @param defaultTransport - The default transport to use.
 * @param relayTransport - The relay transport to use.
 * @param parameters - Configuration parameters.
 * @returns A relay transport.
 */
export function withRelay(
  defaultTransport: Transport,
  relayTransport: Transport,
  parameters?: withRelay.Parameters,
): withRelay.ReturnValue {
  const { policy = 'sign-only' } = parameters ?? {}

  return (config) => {
    const transport_default = defaultTransport(config)
    const transport_relay = relayTransport(config)

    return createTransport({
      key: withRelay.type,
      name: 'Relay Proxy',
      async request({ method, params }, options) {
        if (method === 'eth_fillTransaction')
          return transport_relay.request({ method, params }, options) as never

        if (
          method === 'eth_sendRawTransactionSync' ||
          method === 'eth_sendRawTransaction'
        ) {
          const serialized = (params as any)[0] as `0x76${string}`
          const transaction = Transaction.deserialize(serialized)

          // Serialized Tempo envelopes encode `feePayer: true` as a missing fee payer
          // signature until the relay co-signs the transaction.
          if (transaction.feePayerSignature === null) {
            // For 'sign-and-broadcast', relay signs and broadcasts
            if (policy === 'sign-and-broadcast')
              return transport_relay.request(
                { method, params },
                options,
              ) as never

            // For 'sign-only', request signature from relay using eth_signRawTransaction
            {
              // Request signature from relay using eth_signRawTransaction
              const signedTransaction = await transport_relay.request(
                {
                  method: 'eth_signRawTransaction',
                  params: [serialized],
                },
                options,
              )

              // Broadcast the signed transaction via the default transport
              return transport_default.request(
                { method, params: [signedTransaction] },
                options,
              ) as never
            }
          }
        }

        return (await transport_default.request(
          { method, params },
          options,
        )) as never
      },
      type: withRelay.type,
    })
  }
}

export declare namespace withRelay {
  export const type = 'relay'

  export type Parameters = RelayProxyParameters

  export type ReturnValue = Relay
}

/** @deprecated Use `withRelay` instead. */
export function withFeePayer(
  defaultTransport: Transport,
  relayTransport: Transport,
  parameters?: withFeePayer.Parameters,
): withFeePayer.ReturnValue {
  const { policy = 'sign-only' } = parameters ?? {}

  return (config) => {
    const transport_default = defaultTransport(config)
    const transport_relay = relayTransport(config)

    return createTransport({
      key: withFeePayer.type,
      name: 'Relay Proxy',
      async request({ method, params }, options) {
        if (method === 'eth_fillTransaction') {
          const request = (params as readonly unknown[] | undefined)?.[0]
          if (
            request &&
            typeof request === 'object' &&
            'feePayer' in request &&
            request.feePayer === true
          )
            return transport_relay.request({ method, params }, options) as never
        }
        if (
          method === 'eth_sendRawTransactionSync' ||
          method === 'eth_sendRawTransaction'
        ) {
          const serialized = (params as any)[0] as `0x76${string}`
          const transaction = Transaction.deserialize(serialized)

          // Serialized Tempo envelopes encode `feePayer: true` as a missing fee payer
          // signature until the relay co-signs the transaction.
          if (transaction.feePayerSignature === null) {
            // For 'sign-and-broadcast', relay signs and broadcasts
            if (policy === 'sign-and-broadcast')
              return transport_relay.request(
                { method, params },
                options,
              ) as never

            // For 'sign-only', request signature from relay using eth_signRawTransaction
            {
              // Request signature from relay using eth_signRawTransaction
              const signedTransaction = await transport_relay.request(
                {
                  method: 'eth_signRawTransaction',
                  params: [serialized],
                },
                options,
              )

              // Broadcast the signed transaction via the default transport
              return transport_default.request(
                { method, params: [signedTransaction] },
                options,
              ) as never
            }
          }
        }
        return (await transport_default.request(
          { method, params },
          options,
        )) as never
      },
      type: withFeePayer.type,
    })
  }
}

export declare namespace withFeePayer {
  export const type = 'feePayer'

  export type Parameters = {
    /** Policy for how the fee payer should handle transactions. Defaults to `'sign-only'`. */
    policy?: 'sign-only' | 'sign-and-broadcast' | undefined
  }

  export type ReturnValue = FeePayer
}

/**
 * Creates a transport that instruments a compatibility layer for
 * `wallet_` RPC actions (`sendCalls`, `getCallsStatus`, etc).
 *
 * @param transport - Transport to wrap.
 * @returns Transport.
 */
export function walletNamespaceCompat(
  transport: Transport,
  options: walletNamespaceCompat.Parameters,
): Transport {
  const { account } = options

  const sendCallsMagic = Hash.keccak256(Hex.fromString('TEMPO_5792'))

  return (options) => {
    const t = transport(options)

    const chain = options.chain as Chain & ChainConfig

    return {
      ...t,
      async request(args: never) {
        const request = RpcRequest.from(args)

        const client = createClient({
          chain,
          transport,
        })

        if (request.method === 'wallet_sendCalls') {
          const params = request.params[0] ?? {}
          const { capabilities, chainId, from } = params
          const { sync, ...properties } = capabilities ?? {}

          if (!chainId) throw new Provider.UnsupportedChainIdError()
          if (Number(chainId) !== client.chain.id)
            throw new Provider.UnsupportedChainIdError()
          if (from && !Address.isEqual(from, account.address))
            throw new Provider.DisconnectedError()

          const calls = (params.calls ?? []).map((call) => ({
            to: call.to,
            value: call.value ? BigInt(call.value) : undefined,
            data: call.data,
          }))

          const hash = await (async () => {
            if (!sync)
              return sendTransaction(client, {
                account,
                ...(properties ? properties : {}),
                calls,
              })

            const { transactionHash } = await sendTransactionSync(client, {
              account,
              ...(properties ? properties : {}),
              calls,
            })
            return transactionHash
          })()

          const id = Hex.concat(hash, Hex.padLeft(chainId, 32), sendCallsMagic)

          return {
            capabilities: { sync },
            id,
          }
        }

        if (request.method === 'wallet_getCallsStatus') {
          const [id] = request.params ?? []
          if (!id) throw new Error('`id` not found')
          if (!id.endsWith(sendCallsMagic.slice(2)))
            throw new Error('`id` not supported')
          Hex.assert(id)

          const hash = Hex.slice(id, 0, 32)
          const chainId = Hex.slice(id, 32, 64)

          const receipt = await getTransactionReceipt(client, { hash })
          return {
            atomic: true,
            chainId: Number(chainId),
            id,
            receipts: [receipt],
            status: receipt.status === 'success' ? 200 : 500,
            version: '2.0.0',
          }
        }

        return t.request(args)
      },
    } as never
  }
}

export declare namespace walletNamespaceCompat {
  export type Parameters = {
    account: LocalAccount
  }
}
