import { type ConnectedWallet, useLogin, usePrivy, useWallets } from '@privy-io/react-auth'
import { useEffect, useRef, useSyncExternalStore } from 'react'
import type { Address } from 'viem/accounts'

import type * as Adapter from '../core/Adapter.js'
import { privy as core_privy } from '../core/adapters/privy.js'

/**
 * Creates a Privy adapter wired to `@privy-io/react-auth`.
 *
 * Mount {@link PrivyAccountsBridge} under Privy's `PrivyProvider` in the same
 * React tree. The bridge owns Privy's login modal, hook state, and embedded
 * wallet selection for this adapter.
 */
export function privy(options: privy.Options = {}): Adapter.Adapter {
  return core_privy({
    ...options,
    client,
    loadAccounts: async () => await requestPrivyAccounts(),
  })
}

/** Mounts the React boundary needed by {@link privy}. */
export function PrivyAccountsBridge() {
  const privy = usePrivy()
  const wallets = useWallets()
  const request = usePrivyAccountsRequest()
  const loading = useRef(false)
  const login_started = useRef(false)
  const login = useLogin({
    onError(error) {
      login_started.current = false
      rejectPrivyAccounts(new Error(String(error)))
    },
  })

  useEffect(() => {
    bridge_mounts += 1

    return () => {
      bridge_mounts = Math.max(bridge_mounts - 1, 0)
      if (bridge_mounts === 0)
        rejectPrivyAccounts(
          new Error('PrivyAccountsBridge unmounted before Privy returned accounts.'),
        )
    }
  }, [])

  useEffect(() => {
    setPrivyReactState({
      authenticated: privy.authenticated,
      getAccessToken: privy.getAccessToken,
      logout: privy.logout,
      ready: privy.ready,
      userId: privy.user?.id,
      wallets: wallets.wallets.filter(isEmbeddedEthereumWallet).map((wallet) => ({
        address: wallet.address,
        getEthereumProvider: wallet.getEthereumProvider,
        walletIndex: wallet.walletIndex,
      })),
      walletsReady: wallets.ready,
    })
  }, [
    privy.authenticated,
    privy.getAccessToken,
    privy.logout,
    privy.ready,
    privy.user,
    wallets.ready,
    wallets.wallets,
  ])

  useEffect(() => {
    if (!request || loading.current || !privy.ready) return

    if (!privy.authenticated) {
      if (!login_started.current) {
        login_started.current = true
        login.login()
      }
      return
    }

    login_started.current = false
    const embedded = wallets.wallets.filter(isEmbeddedEthereumWallet)
    if (!wallets.ready && embedded.length === 0) return

    loading.current = true
    void (async () => {
      try {
        if (embedded.length === 0)
          throw new Error('Privy React returned no embedded Ethereum wallet.')

        resolvePrivyAccounts(embedded.map((wallet) => wallet.address as Address))
      } catch (error) {
        rejectPrivyAccounts(error instanceof Error ? error : new Error(String(error)))
      } finally {
        loading.current = false
      }
    })()
  }, [login, privy.authenticated, privy.ready, privy.user, request, wallets.ready, wallets.wallets])

  return null
}

type PrivyReactWallet = {
  address: string
  getEthereumProvider: () => Promise<core_privy.EthereumProvider> | core_privy.EthereumProvider
  walletIndex?: number | undefined
}

type PrivyReactState = {
  authenticated: boolean
  getAccessToken?: (() => Promise<string | null>) | undefined
  logout?: (() => Promise<void>) | undefined
  ready: boolean
  userId?: string | undefined
  wallets: readonly PrivyReactWallet[]
  walletsReady: boolean
}

type PrivyAccountsRequest = {
  reject: (error: Error) => void
  resolve: (accounts: Exclude<core_privy.AccountSelection, void>) => void
}

let request: PrivyAccountsRequest | undefined
let state: PrivyReactState = {
  authenticated: false,
  ready: false,
  wallets: [],
  walletsReady: false,
}
let bridge_mounts = 0
const listeners = new Set<() => void>()
const ready_listeners = new Set<() => void>()
const wallets_ready_listeners = new Set<() => void>()

const client = {
  auth: {
    async logout() {
      await state.logout?.()
    },
  },
  embeddedWallet: {
    async getEthereumProvider(parameters: {
      entropyId: string
      entropyIdVerifier: string
      wallet: core_privy.LinkedAccount
    }) {
      await waitForWallets()

      const address = parameters.wallet.address
      if (!address) throw new Error('Privy React wallet address is required.')

      const wallet = state.wallets.find((wallet) => sameAddress(wallet.address, address))
      if (!wallet) throw new Error(`Privy React wallet "${address}" is no longer connected.`)

      return await wallet.getEthereumProvider()
    },
  },
  async getAccessToken() {
    await waitForReady()
    if (!state.authenticated) return null
    return (await state.getAccessToken?.()) ?? null
  },
  initialize() {},
  user: {
    async get() {
      await waitForWallets()
      if (!state.authenticated) throw new Error('Privy React user is not authenticated.')

      return {
        user: {
          id: state.userId ?? 'privy-react-user',
          linked_accounts: state.wallets.map((wallet, index) => ({
            address: wallet.address,
            chain_type: 'ethereum',
            connector_type: 'embedded',
            type: 'wallet',
            wallet_client_type: 'privy',
            wallet_index: wallet.walletIndex ?? index,
          })),
        },
      }
    },
  },
} satisfies core_privy.Client

function setPrivyReactState(next: PrivyReactState) {
  state = next
  if (next.ready) emitReady()
  if (next.ready && (next.walletsReady || next.wallets.length > 0)) emitWalletsReady()
}

function usePrivyAccountsRequest() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
}

function requestPrivyAccounts() {
  if (bridge_mounts === 0) throw createMissingBridgeError()
  if (request) request.reject(new Error('Another Privy React request is already active.'))

  return new Promise<Exclude<core_privy.AccountSelection, void>>((resolve, reject) => {
    request = { reject, resolve }
    emit()
  })
}

function resolvePrivyAccounts(accounts: Exclude<core_privy.AccountSelection, void>) {
  request?.resolve(accounts)
  request = undefined
  emit()
}

function rejectPrivyAccounts(error: Error) {
  request?.reject(error)
  request = undefined
  emit()
}

function emit() {
  for (const listener of listeners) listener()
}

function emitReady() {
  for (const listener of ready_listeners) listener()
  ready_listeners.clear()
}

function emitWalletsReady() {
  for (const listener of wallets_ready_listeners) listener()
  wallets_ready_listeners.clear()
}

function createMissingBridgeError() {
  return new Error('PrivyAccountsBridge must be mounted under PrivyProvider to use privy().')
}

function getSnapshot() {
  return request
}

function isEmbeddedEthereumWallet(wallet: ConnectedWallet) {
  return (
    wallet.type === 'ethereum' &&
    wallet.walletClientType === 'privy' &&
    wallet.connectorType === 'embedded'
  )
}

function sameAddress(a: string, b: string) {
  return a.toLowerCase() === b.toLowerCase()
}

function subscribe(listener: () => void) {
  listeners.add(listener)
  return () => listeners.delete(listener)
}

function waitForReady() {
  if (state.ready) return Promise.resolve()
  return new Promise<void>((resolve) => ready_listeners.add(resolve))
}

async function waitForWallets() {
  await waitForReady()
  if (state.walletsReady || state.wallets.length > 0) return
  return new Promise<void>((resolve) => wallets_ready_listeners.add(resolve))
}

export declare namespace privy {
  /** Options for {@link privy}. */
  type Options = {
    /** Data URI of the provider icon. @default Black 1×1 SVG. */
    icon?: `data:image/${string}` | undefined
    /** Display name of the provider. @default "Privy" */
    name?: string | undefined
    /** Reverse DNS identifier. @default "io.privy" */
    rdns?: string | undefined
  }
}
