import {
  Address as core_Address,
  Base64,
  Hex,
  P256,
  Provider as core_Provider,
  PublicKey,
  RpcResponse,
} from 'ox'
import { KeyAuthorization } from 'ox/tempo'
import { prepareTransactionRequest } from 'viem/actions'
import { Actions, Account as TempoAccount, Secp256k1 } from 'viem/tempo'

import * as AccessKey from '../core/AccessKey.js'
import * as Adapter from '../core/Adapter.js'
import type * as Storage from '../core/Storage.js'

/**
 * Creates a React Native adapter that authorizes access keys via the system browser.
 *
 * Authentication opens a browser session and completes via a redirect callback
 * that returns the signed key authorization.
 */
export function reactNative(options: reactNative.Options): Adapter.Adapter {
  const { name = 'Tempo Mobile', rdns = 'xyz.tempo.mobile' } = options

  return Adapter.define({ name, rdns }, ({ getAccount, getClient, store }) => {
    async function loadManagedKey(
      address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
      parameters: loadManagedKey.Options = {},
    ): Promise<loadManagedKey.ReturnType | undefined> {
      const { keyType } = parameters
      const { secureStorage } = options
      if (!secureStorage) return undefined

      const { chainId } = store.getState()
      const storageKeys = keyType
        ? [managedKeyStorageKey(address, chainId, keyType)]
        : [
            managedKeyStorageKey(address, chainId, 'secp256k1'),
            managedKeyStorageKey(address, chainId, 'p256'),
            managedKeyStorageKey(address, chainId),
          ]
      let entry: ManagedKeyEntry | null = null
      for (const storageKey of storageKeys) {
        entry = await secureStorage.getItem<ManagedKeyEntry>(storageKey)
        if (entry) break
      }
      if (!entry) return undefined

      const account =
        entry.keyType === 'p256'
          ? TempoAccount.fromP256(entry.key, { access: address })
          : TempoAccount.fromSecp256k1(entry.key, { access: address })
      const keyAddress = core_Address.fromPublicKey(PublicKey.from(account.publicKey))
      const deserialized = KeyAuthorization.deserialize(entry.keyAuthorization)
      if (!deserialized.signature) throw new Error('Managed access key is missing a signature.')
      const keyAuthorization = deserialized as KeyAuthorization.Signed

      if (keyAuthorization.address.toLowerCase() === keyAddress.toLowerCase())
        AccessKey.save({
          address,
          keyAuthorization,
          privateKey: entry.key,
          store,
        })
      else
        store.setState((state) => ({
          accessKeys: state.accessKeys.filter(
            (accessKey) =>
              accessKey.address.toLowerCase() !== keyAuthorization.address.toLowerCase(),
          ),
        }))

      return {
        account,
        expiry: entry.expiry,
        key: entry.key,
        keyAddress,
        keyType: entry.keyType,
        publicKey: account.publicKey,
        storedAuthorization: keyAuthorization,
      }
    }

    async function resolveManagedKey(
      resolveOptions: {
        address?: Adapter.authorizeAccessKey.ReturnType['rootAddress'] | undefined
        keyType?: Adapter.authorizeAccessKey.Parameters['keyType'] | undefined
      } = {},
    ): Promise<resolveManagedKey.ReturnType> {
      const { address, keyType } = resolveOptions

      const requestedKeyType = keyType === 'p256' || keyType === 'secp256k1' ? keyType : undefined
      const entry = address
        ? await loadManagedKey(address, requestedKeyType ? { keyType: requestedKeyType } : {})
        : undefined
      if (entry)
        return {
          account: entry.account,
          key: entry.key,
          keyAddress: entry.keyAddress,
          keyType: entry.keyType,
          publicKey: entry.publicKey,
        }

      const nextKeyType = requestedKeyType === 'p256' ? 'p256' : 'secp256k1'
      const key = nextKeyType === 'p256' ? P256.randomPrivateKey() : Secp256k1.randomPrivateKey()
      const account =
        nextKeyType === 'p256'
          ? TempoAccount.fromP256(key, address ? { access: address } : undefined)
          : TempoAccount.fromSecp256k1(key, address ? { access: address } : undefined)

      return {
        account,
        key,
        keyAddress: core_Address.fromPublicKey(PublicKey.from(account.publicKey)),
        keyType: nextKeyType,
        publicKey: account.publicKey,
      }
    }

    async function saveManagedKey(
      address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
      managedKey: Awaited<ReturnType<typeof resolveManagedKey>>,
      keyAuthorization: KeyAuthorization.Signed,
    ) {
      if (!managedKey) return

      AccessKey.save({
        address,
        keyAuthorization,
        privateKey: managedKey.key,
        store,
      })

      const { secureStorage } = options
      if (!secureStorage) return

      const { chainId } = store.getState()
      const storageKey = managedKeyStorageKey(address, chainId, managedKey.keyType)
      const entry: ManagedKeyEntry = {
        chainId,
        expiry: keyAuthorization.expiry ?? 0,
        key: managedKey.key,
        keyAddress: managedKey.keyAddress,
        keyAuthorization: KeyAuthorization.serialize(keyAuthorization),
        keyType: managedKey.keyType,
        walletAddress: address,
      }
      await secureStorage.setItem(storageKey, entry)
    }

    async function isManagedKeyAuthorized(
      address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
      managedKey: loadManagedKey.ReturnType,
    ) {
      try {
        const metadata = await Actions.accessKey.getMetadata(getClient(), {
          account: address,
          accessKey: managedKey.keyAddress,
        })
        return (
          metadata.address.toLowerCase() === managedKey.keyAddress.toLowerCase() &&
          !metadata.isRevoked
        )
      } catch {
        return false
      }
    }

    async function reauthorizeManagedKey(
      address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
      managedKey: loadManagedKey.ReturnType,
    ) {
      const result = await authorize({
        account: address,
        authorizeAccessKey: {
          expiry: managedKey.expiry,
          keyType: managedKey.keyType,
          ...(managedKey.storedAuthorization.limits
            ? { limits: managedKey.storedAuthorization.limits.map((limit) => ({ ...limit })) }
            : {}),
          publicKey: managedKey.publicKey,
        },
        method: 'wallet_authorizeAccessKey',
      })
      await saveManagedKey(address, managedKey, result.keyAuthorization)
      return result.keyAuthorization
    }

    async function withManagedAccessKey<result>(
      fn: (
        account: TempoAccount.Account,
        keyAuthorization?: KeyAuthorization.Signed | undefined,
      ) => Promise<result>,
    ) {
      const rootAddress = store.getState().accounts[store.getState().activeAccount]?.address
      const managedKey = rootAddress ? await loadManagedKey(rootAddress) : undefined

      const account = managedKey?.account ?? getAccount({ signable: true })
      let keyAuthorization = AccessKey.getPending(account, { store })
      if (rootAddress && managedKey && !keyAuthorization)
        if (!(await isManagedKeyAuthorized(rootAddress, managedKey)))
          keyAuthorization = await reauthorizeManagedKey(rootAddress, managedKey)

      try {
        return await fn(account, keyAuthorization ?? undefined)
      } catch (error) {
        AccessKey.remove(account, { store })
        throw error
      }
    }

    async function authorize(request: {
      account?: Adapter.authorizeAccessKey.ReturnType['rootAddress'] | undefined
      authorizeAccessKey: Adapter.authorizeAccessKey.Parameters | undefined
      method: 'wallet_authorizeAccessKey' | 'wallet_connect'
    }) {
      const { host, redirectUri, open = defaultOpen } = options
      const { account, authorizeAccessKey, method } = request

      const managedKey =
        authorizeAccessKey && !authorizeAccessKey.publicKey && !authorizeAccessKey.address
          ? await resolveManagedKey({
              ...(account ? { address: account } : {}),
              ...(authorizeAccessKey.keyType ? { keyType: authorizeAccessKey.keyType } : {}),
            })
          : undefined

      const publicKey = authorizeAccessKey?.publicKey ?? managedKey?.publicKey
      const keyType = authorizeAccessKey?.keyType ?? managedKey?.keyType

      if (!publicKey)
        throw new RpcResponse.InvalidParamsError({
          message:
            method === 'wallet_connect'
              ? '`wallet_connect` on the React Native adapter requires `capabilities.authorizeAccessKey`.'
              : '`wallet_authorizeAccessKey` on the React Native adapter requires key parameters.',
        })

      const state = Base64.fromBytes(Hex.toBytes(Hex.random(16)), { pad: false, url: true })
      const authUrl = buildAuthUrl(host, {
        callback: redirectUri,
        chainId: store.getState().chainId,
        ...(typeof authorizeAccessKey?.expiry !== 'undefined'
          ? { expiry: authorizeAccessKey.expiry }
          : {}),
        ...(keyType ? { keyType } : {}),
        ...(authorizeAccessKey?.limits
          ? { limits: authorizeAccessKey.limits.map((l) => ({ ...l, limit: String(l.limit) })) }
          : {}),
        pubKey: publicKey,
        state,
      })

      const callbackUrl = await open(authUrl, redirectUri)
      if (!callbackUrl) throw new AuthCancelledError()

      const params = new URL(callbackUrl).searchParams
      const returnedState = params.get('state')
      if (returnedState !== state) throw new StateMismatchError()

      const accountAddress = params.get('accountAddress')
      if (!accountAddress) throw new Error('Missing accountAddress in callback.')

      const keyAuthorizationHex = params.get('keyAuthorization')
      if (!keyAuthorizationHex) throw new Error('Missing keyAuthorization in callback.')

      const keyAuthorization = KeyAuthorization.deserialize(keyAuthorizationHex as Hex.Hex)
      if (!keyAuthorization.signature)
        throw new Error('Key authorization in callback is missing a signature.')
      const signed = keyAuthorization as KeyAuthorization.Signed

      if (managedKey)
        await saveManagedKey(accountAddress as core_Address.Address, managedKey, signed)

      return {
        accountAddress: accountAddress as core_Address.Address,
        keyAuthorization: signed,
      }
    }

    return {
      actions: {
        async authorizeAccessKey(parameters) {
          const { accounts, activeAccount } = store.getState()
          const account = accounts[activeAccount]?.address
          const result = await authorize({
            ...(account ? { account } : {}),
            authorizeAccessKey: parameters,
            method: 'wallet_authorizeAccessKey',
          })

          if (!account)
            store.setState({
              accounts: [{ address: result.accountAddress }],
              activeAccount: 0,
            })

          return {
            keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization),
            rootAddress: result.accountAddress,
          }
        },
        async createAccount(params, request) {
          return this.loadAccounts(params, request)
        },
        async loadAccounts(parameters) {
          if (parameters?.digest)
            throw unsupported(
              '`wallet_connect` digest signing not supported by React Native adapter.',
            )

          const result = await authorize({
            authorizeAccessKey: parameters?.authorizeAccessKey,
            method: 'wallet_connect',
          })

          return {
            accounts: [
              {
                address: result.accountAddress,
                capabilities: {},
              },
            ],
            keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization),
          }
        },
        async revokeAccessKey() {
          throw unsupported('`wallet_revokeAccessKey` not supported by React Native adapter.')
        },
        async sendTransaction(parameters) {
          const { feePayer, ...rest } = parameters
          const client = getClient(typeof feePayer === 'string' ? { feePayer } : {})
          const { account, prepared } = await withManagedAccessKey(
            async (account, keyAuthorization) => ({
              account,
              prepared: await prepareTransactionRequest(client, {
                account,
                ...rest,
                ...(feePayer ? { feePayer: true } : {}),
                ...(keyAuthorization ? { keyAuthorization } : {}),
                type: 'tempo',
              } as never),
            }),
          )
          const signed = await account.signTransaction(prepared as never)
          const result = await client.request({
            method: 'eth_sendRawTransaction' as never,
            params: [signed],
          })
          AccessKey.removePending(account, { store })
          return result
        },
        async sendTransactionSync(parameters) {
          const { feePayer, ...rest } = parameters
          const client = getClient(typeof feePayer === 'string' ? { feePayer } : {})
          const { account, prepared } = await withManagedAccessKey(
            async (account, keyAuthorization) => ({
              account,
              prepared: await prepareTransactionRequest(client, {
                account,
                ...rest,
                ...(feePayer ? { feePayer: true } : {}),
                ...(keyAuthorization ? { keyAuthorization } : {}),
                type: 'tempo',
              } as never),
            }),
          )
          const signed = await account.signTransaction(prepared as never)
          const result = await client.request({
            method: 'eth_sendRawTransactionSync' as never,
            params: [signed],
          })
          AccessKey.removePending(account, { store })
          return result
        },
        async signPersonalMessage({ address, data }) {
          await loadManagedKey(address)
          const account = getAccount({ address, signable: true })
          return await account.signMessage({ message: { raw: data } })
        },
        async signTransaction(parameters) {
          const { feePayer, ...rest } = parameters
          const client = getClient(typeof feePayer === 'string' ? { feePayer } : {})
          const { account, prepared } = await withManagedAccessKey(
            async (account, keyAuthorization) => ({
              account,
              prepared: await prepareTransactionRequest(client, {
                account,
                ...rest,
                ...(feePayer ? { feePayer: true } : {}),
                ...(keyAuthorization ? { keyAuthorization } : {}),
                type: 'tempo',
              } as never),
            }),
          )
          return await account.signTransaction(prepared as never)
        },
        async signTypedData({ address, data }) {
          await loadManagedKey(address)
          const account = getAccount({ address, signable: true })
          return await account.signTypedData(JSON.parse(data) as never)
        },
      },
    }
  })
}

export declare namespace reactNative {
  export type Options = {
    /** Host URL for the mobile auth page. @default "https://wallet-next.tempo.xyz" */
    host: string
    /** Provider display name. @default "Tempo Mobile" */
    name?: string | undefined
    /**
     * Browser opener override. Opens the auth URL and returns the callback URL.
     * @default Uses `expo-web-browser`'s `openAuthSessionAsync`.
     */
    open?: ((url: string, redirectUri: string) => Promise<string | null>) | undefined
    /** Redirect URI for the auth callback (e.g. your app's deep link scheme). */
    redirectUri: string
    /** Reverse-DNS provider identifier. @default "xyz.tempo.mobile" */
    rdns?: string | undefined
    /** Secure storage adapter for persisting managed access keys. */
    secureStorage?: Storage.Storage | undefined
  }
}

declare namespace resolveManagedKey {
  type ReturnType = {
    account: TempoAccount.Account
    key: Hex.Hex
    keyAddress: core_Address.Address
    keyType: 'secp256k1' | 'p256'
    publicKey: Hex.Hex
  }
}

declare namespace loadManagedKey {
  type Options = {
    keyType?: 'secp256k1' | 'p256' | undefined
  }

  type ReturnType = resolveManagedKey.ReturnType & {
    expiry: number
    storedAuthorization: KeyAuthorization.Signed
  }
}

/** Entry shape persisted to secure storage for managed access keys. */
type ManagedKeyEntry = {
  chainId: number
  expiry: number
  key: Hex.Hex
  keyAddress: core_Address.Address
  keyAuthorization: Hex.Hex
  keyType: 'secp256k1' | 'p256'
  walletAddress: core_Address.Address
}

class AuthCancelledError extends Error {
  constructor() {
    super('Authentication was cancelled by the user.')
    this.name = 'AuthCancelledError'
  }
}

class StateMismatchError extends Error {
  constructor() {
    super('State parameter mismatch — possible CSRF attack.')
    this.name = 'StateMismatchError'
  }
}

async function defaultOpen(url: string, redirectUri: string): Promise<string | null> {
  const { openAuthSessionAsync } = await import('expo-web-browser')
  const result = await openAuthSessionAsync(url, redirectUri)
  if (result.type !== 'success') return null
  return result.url
}

function buildAuthUrl(
  host: string,
  params: {
    callback: string
    chainId: number
    expiry?: number | undefined
    keyType?: string | undefined
    limits?: readonly { token: string; limit: string }[] | undefined
    pubKey: Hex.Hex
    state: string
  },
): string {
  // TODO: use the new host
  // const url = new URL('/remote/auth/mobile', host)
  const url = new URL('/mobile-auth', host)
  url.searchParams.set('pubKey', params.pubKey)
  if (params.keyType) url.searchParams.set('keyType', params.keyType)
  url.searchParams.set('chainId', String(params.chainId))
  if (typeof params.expiry !== 'undefined') url.searchParams.set('expiry', String(params.expiry))
  if (params.limits) url.searchParams.set('limits', JSON.stringify(params.limits))
  url.searchParams.set('callback', params.callback)
  url.searchParams.set('state', params.state)
  return url.toString()
}

function managedKeyStorageKey(
  address: core_Address.Address,
  chainId: number,
  keyType?: string | undefined,
): string {
  return `managedKey.${address.toLowerCase()}.${chainId}${keyType ? `.${keyType}` : ''}`
}

function unsupported(message: string) {
  return new core_Provider.UnsupportedMethodError({ message })
}
