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 HttpTransport,
  type HttpTransportConfig,
  http as http_,
} from '../clients/transports/http.js'
import {
  MethodNotFoundRpcError,
  MethodNotSupportedRpcError,
} from '../errors/rpc.js'
import type { Chain } from '../types/chain.js'
import type { ChainConfig } from './chainConfig.js'
import * as Multisig from './Multisig.js'
import type { Store } from './Store.js'
import * as Store_ from './Store.js'
import * as Transaction from './Transaction.js'

export type HttpConfig = Omit<
  HttpTransportConfig,
  'batch' | 'raw' | 'rpcSchema'
> & {
  /** Store for reading Zone authorization tokens. Defaults to sessionStorage (web) or memory (server). */
  store?: Store | undefined
}

/**
 * Creates an HTTP transport with support for Zone authentication tokens.
 *
 * Reads the authorization token from the store and injects the
 * `X-Authorization-Token` header on every request.
 *
 * @example
 * ```ts
 * import { createPublicClient } from 'viem'
 * import { http, Zone } from 'viem/tempo'
 *
 * const client = createPublicClient({
 *   chain: Zone.a,
 *   transport: http(),
 * })
 * ```
 */
export function http(
  url?: string | undefined,
  config: HttpConfig = {},
): HttpTransport {
  const { store: store_, onFetchRequest, ...rest } = config
  const store = store_ ?? Store_.defaultStore()

  return (config) =>
    http_(url, {
      ...rest,
      async onFetchRequest(request, init) {
        const next = (await onFetchRequest?.(request, init)) ?? init
        const headers = new Headers(next.headers)

        const chainId = config.chain?.id
        if (chainId) {
          const token = (await store.getItem(`auth:token:${chainId}`)) ?? null
          if (token) headers.set('X-Authorization-Token', token)
        }

        return { ...next, headers }
      },
    })(config)
}

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, { multisig: true }>

/**
 * Wraps a transport with native multisig request handling.
 *
 * @example
 * ```ts
 * import { http, Store, withMultisig } from 'viem/tempo'
 *
 * const transport = withMultisig(http(), {
 *   store: Store.memory(),
 * })
 * ```
 *
 * @param transport - Transport to wrap.
 * @param parameters - Multisig request handler parameters.
 * @returns The wrapped transport.
 * @experimental
 */
export function withMultisig<transport extends Transport>(
  transport: transport,
  parameters: withMultisig.Parameters,
): withMultisig.ReturnValue<transport> {
  return ((options: Parameters<Transport>[0]) => {
    const value = transport(options)
    return {
      ...value,
      request: Multisig.handleRequest(
        (request, requestOptions) =>
          value.request(request as never, requestOptions),
        parameters,
      ) as typeof value.request,
      value: { ...value.value, multisig: true },
    }
  }) as withMultisig.ReturnValue<transport>
}

export declare namespace withMultisig {
  /** Multisig transport parameters. */
  export type Parameters = Multisig.handleRequest.Parameters

  /** Multisig transport return type. */
  export type ReturnValue<transport extends Transport = Transport> =
    transport extends Transport<
      infer type,
      infer rpcAttributes,
      infer eip1193RequestFn
    >
      ? Transport<
          type,
          rpcAttributes & {
            /** Whether the transport coordinates native multisig approvals. */
            multisig: true
          },
          eip1193RequestFn
        >
      : never
}

/**
 * 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.
 * Multisig approvals, configs, operations, and operation-aware transaction
 * lookups are also sent to the relay so it can coordinate approvals in its store.
 *
 * 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)

    const transport = 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_getTransactionByHash' ||
          method === 'eth_getTransactionReceipt'
        ) {
          const result = await transport_default.request(
            { method, params },
            options,
          )
          if (result !== null && typeof result !== 'undefined')
            return result as never

          const operation = await (async () => {
            try {
              return await transport_relay.request(
                { method: 'multisig_getOperation', params },
                options,
              )
            } catch (error) {
              if (
                error instanceof MethodNotFoundRpcError ||
                error instanceof MethodNotSupportedRpcError
              ) {
                // Relays created before multisig coordination do not expose this method.
                return null
              }
              throw error
            }
          })()
          if (
            !operation ||
            typeof operation !== 'object' ||
            !('type' in operation) ||
            operation.type !== 'transaction'
          )
            return result as never
          return transport_relay.request({ method, params }, options) as never
        }

        if (
          method === 'multisig_approveKeyAuthorization' ||
          method === 'multisig_approveRawTransaction' ||
          method === 'multisig_approveRawTransactionSync' ||
          method === 'multisig_getConfig' ||
          method === 'multisig_getOperation'
        )
          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)

          if (transaction.signature?.type === 'multisig')
            return transport_relay.request({ method, params }, options) as never

          // 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,
    })
    return { ...transport, value: { multisig: true } }
  }
}

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
  }
}
