import { type Provider as ox_Provider } from 'ox'
import {
  type Chain,
  createClient,
  type Client,
  type EIP1193RequestFn,
  http,
  type Transport,
} from 'viem'
import type { tempo } from 'viem/chains'
import { Transaction } from 'viem/tempo'

import type * as Store from './Store.js'

const defaultClients = new Map<string, Client>()
const clients = new WeakMap<object, Map<string, Client>>()

/** Resolves a viem Client for a given chain ID (cached). */
export function fromChainId(
  chainId: number | undefined,
  options: fromChainId.Options,
): Client<Transport, typeof tempo> {
  const { chains, feePayer: feePayerOption, provider, store, transports } = options
  const feePayerUrl = (() => {
    if (feePayerOption === false) return undefined
    if (typeof feePayerOption === 'string') return normalizeFeePayerUrl(feePayerOption)
    if (feePayerOption?.url) return normalizeFeePayerUrl(feePayerOption.url)
    return undefined
  })()
  const precedence = (() => {
    if (typeof feePayerOption === 'object' && feePayerOption !== null)
      return feePayerOption.precedence ?? 'fee-payer-first'
    return 'fee-payer-first'
  })()
  const id = chainId ?? store.getState().chainId
  const key = `${id}:${provider ? 'p' : ''}:${feePayerOption === false ? 'no-fp' : (feePayerUrl ?? '')}:${precedence}`
  // Scope the cache by `provider` (preferred) or `transports`, falling back
  // to a shared module-level map. The cache key only encodes a boolean for
  // `provider`, so without scoping, two providers sharing the same chainId
  // would hit each other's cached clients and route requests to the wrong
  // adapter via `providerTransport`.
  const scope = provider ?? transports
  const cache = (() => {
    if (!scope) return defaultClients
    let map = clients.get(scope)
    if (!map) {
      map = new Map()
      clients.set(scope, map)
    }
    return map
  })()
  let client = cache.get(key)
  if (!client) {
    const chain = chains.find((c) => c.id === id) ?? chains[0]!
    const base = transports?.[id] ?? http()
    const transport_base = provider ? providerTransport(provider, base) : base
    const transport = feePayerUrl
      ? feePayerTransport(transport_base, feePayerUrl, precedence)
      : transport_base
    client = createClient({ chain, transport, pollingInterval: 1000 })
    cache.set(key, client)
  }
  return client as never
}

export declare namespace fromChainId {
  type Options = {
    /** Supported chains. */
    chains: readonly [Chain, ...Chain[]]
    /** Fee payer configuration. A URL string, config object, or `false` to opt out. */
    feePayer?:
      | string
      | false
      | {
          /** Fee payer service URL. */
          url: string
          /** Signing precedence. @default 'fee-payer-first' */
          precedence?: 'fee-payer-first' | 'user-first' | undefined
        }
      | undefined
    /** Provider instance. When set, the transport routes requests through the provider first, falling back to HTTP for unknown methods. */
    provider?: ox_Provider.Provider | undefined
    /** Reactive state store. */
    store: Store.Store
    /** Per-chain transports keyed by chain ID. When omitted, defaults to `http()` (uses the chain's default RPC URL). */
    transports?: Record<number, Transport> | undefined
  }
}

/**
 * Creates a transport that routes requests through the provider, falling
 * back to the given base transport for methods the provider proxies to RPC.
 */
function providerTransport(provider: ox_Provider.Provider, base: Transport): Transport {
  return (params) => {
    const baseTransport = base(params)
    return {
      ...baseTransport,
      async request({ method, params: reqParams }) {
        return (provider as { request: EIP1193RequestFn }).request({
          method,
          params: reqParams,
        } as any)
      },
    } as ReturnType<Transport>
  }
}

/**
 * Resolves a fee payer URL to an absolute URL string. Relative paths (e.g.
 * `/relay`) are resolved against `window.location.origin` when running in a
 * browser; on the server, relative paths are returned as-is.
 */
function normalizeFeePayerUrl(url: string): string {
  if (url.startsWith('http://') || url.startsWith('https://')) return url
  if (typeof window !== 'undefined') return new URL(url, window.location.origin).href
  return url
}

function feePayerTransport(
  base: Transport,
  url: string,
  precedence: 'fee-payer-first' | 'user-first',
): Transport {
  return (params) => {
    const baseTransport = base(params)
    const sponsor = http(url)(params)

    return {
      ...baseTransport,
      async request({ method, params: rpcParams }: { method: string; params?: unknown }) {
        const args = rpcParams as readonly unknown[] | undefined

        if (precedence === 'fee-payer-first' && method === 'eth_fillTransaction') {
          const request = args?.[0]
          if (
            request &&
            typeof request === 'object' &&
            'feePayer' in request &&
            (request.feePayer === true || typeof request.feePayer === 'string')
          )
            return sponsor.request({
              method,
              params: [{ ...request, feePayer: true }],
            })
        }

        if (method === 'eth_sendRawTransaction' || method === 'eth_sendRawTransactionSync') {
          const serialized = args?.[0]
          if (
            typeof serialized === 'string' &&
            (serialized.startsWith('0x76') || serialized.startsWith('0x78'))
          ) {
            const deserialized = Transaction.deserialize(serialized as `0x76${string}`)
            if ('feePayerSignature' in deserialized && deserialized.feePayerSignature === null) {
              const signed = await sponsor.request({
                method: 'eth_signRawTransaction',
                params: [serialized],
              })
              return await baseTransport.request({ method, params: [signed] })
            }
          }
        }

        return await baseTransport.request({ method, params: rpcParams })
      },
    } as ReturnType<Transport>
  }
}
