import {
  Address as core_Address,
  Bytes,
  Hex,
  Provider as ox_Provider,
  PublicKey,
  RpcResponse,
  Secp256k1,
} from 'ox'
import { KeyAuthorization } from 'ox/tempo'
import { hashMessage, hashTypedData, isAddressEqual } from 'viem'
import type { Address } from 'viem/accounts'
import { prepareTransactionRequest } from 'viem/actions'
import { Account as TempoAccount } from 'viem/tempo'

import * as AccessKey from '../AccessKey.js'
import * as Adapter from '../Adapter.js'
import * as Store from '../Store.js'

const turnkeySessionErrorCodes = new Set([
  'API_KEY_EXPIRED',
  'NO_SESSION_FOUND',
  'REQUEST_NOT_AUTHORIZED',
  'SESSION_EXPIRED',
  'SIGNATURE_INVALID',
  'SIGNATURE_MISSING',
  'UNAUTHENTICATED',
  'UNAUTHORIZED',
])

/**
 * Creates a Turnkey adapter backed by `@turnkey/core` client sessions and Ethereum wallet accounts.
 *
 * The adapter owns silent reconnect, session-expiry cleanup, and provider signing actions.
 * Apps provide the UI-bearing login or sign-up flow through `loadAccounts`. The adapter
 * fetches Ethereum wallet accounts from Turnkey after the flow completes. Provide
 * `createAccount` only when registration needs a distinct Turnkey flow.
 *
 * @example
 * ```ts
 * import { TurnkeyClient, generateWalletAccountsFromAddressFormat } from '@turnkey/core'
 * import { Provider, turnkey } from 'accounts'
 *
 * const provider = Provider.create({
 *   adapter: turnkey({
 *     client: new TurnkeyClient({ organizationId, authProxyConfigId }),
 *     createAccount: async ({ client, parameters }) => {
 *       await client.signUpWithPasskey({
 *         passkeyDisplayName: parameters.name,
 *         createSubOrgParams: {
 *           userName: parameters.name,
 *           customWallet: {
 *             walletName: 'FooBar',
 *             walletAccounts: generateWalletAccountsFromAddressFormat({
 *               addresses: ['ADDRESS_FORMAT_ETHEREUM'],
 *             }),
 *           },
 *         },
 *       })
 *     },
 *     loadAccounts: async ({ client }) => {
 *       await client.loginWithPasskey()
 *     },
 *   }),
 * })
 * ```
 */
export function turnkey<const client extends turnkey.Client>(
  options: turnkey.Options<client>,
): Adapter.Adapter {
  const { icon, name = 'Turnkey', rdns = 'com.turnkey', sessionSkewMs = 10_000 } = options

  return Adapter.define({ icon, name, rdns }, ({ getAccount, getClient, store }) => {
    let turnkeyClient_promise: Promise<client> | undefined
    let expiry_timeout: ReturnType<typeof setTimeout> | undefined
    let restore_promise: Promise<void> | undefined
    let walletAccounts: readonly turnkey.WalletAccount[] = []

    async function getTurnkeyClient(): Promise<client> {
      turnkeyClient_promise ??= (async () => {
        const { client } = options
        await client.init?.()
        return client
      })()
      return await turnkeyClient_promise
    }

    function toStoreAccount(account: turnkey.WalletAccount, label?: string | undefined) {
      return {
        address: core_Address.from(account.address),
        ...(label ? { label } : {}),
      }
    }

    function toTempoAccount(account: turnkey.WalletAccount): TempoAccount.Account {
      const publicKey = toPublicKey(account)
      assertAddress(account, publicKey)

      const sign = async (parameters: { hash: Hex.Hex }) =>
        await signPayload({
          payload: parameters.hash,
          turnkeyClient: await getTurnkeyClient(),
          walletAccount: account,
        })

      return TempoAccount.from({
        keyType: 'secp256k1',
        publicKey,
        sign,
      })
    }

    function toPublicKey(account: turnkey.WalletAccount) {
      const publicKey = account.publicKey.startsWith('0x')
        ? account.publicKey
        : `0x${account.publicKey}`
      Hex.assert(publicKey, { strict: true })
      return PublicKey.from(Secp256k1.noble.ProjectivePoint.fromHex(Bytes.fromHex(publicKey)))
    }

    function assertAddress(account: turnkey.WalletAccount, publicKey: PublicKey.PublicKey) {
      const address = core_Address.from(account.address)
      const address_publicKey = core_Address.fromPublicKey(publicKey)
      if (isAddressEqual(address, address_publicKey)) return

      throw new RpcResponse.InternalError({
        message: `Turnkey account publicKey does not match address "${address}".`,
      })
    }

    async function fetchWalletAccounts(): Promise<readonly turnkey.WalletAccount[]> {
      const turnkeyClient = await getTurnkeyClient()
      return (await turnkeyClient.fetchWallets()).flatMap((wallet) =>
        wallet.accounts.filter((account) => account.addressFormat === 'ADDRESS_FORMAT_ETHEREUM'),
      )
    }

    function selectWalletAccounts(
      accounts: readonly turnkey.WalletAccount[],
      addresses: turnkey.AccountSelection,
    ) {
      if (!addresses) return accounts

      return addresses.map((address) => {
        const address_ = core_Address.from(address)
        const account = accounts.find((account) =>
          isAddressEqual(core_Address.from(account.address), address_),
        )
        if (account) return account

        throw new RpcResponse.InternalError({
          message: `Turnkey callback returned address "${address_}" that was not found in fetched wallet accounts.`,
        })
      })
    }

    function clear() {
      if (expiry_timeout) clearTimeout(expiry_timeout)
      expiry_timeout = undefined
      restore_promise = undefined
      walletAccounts = []
      store.setState({ accessKeys: [], accounts: [], activeAccount: 0 })
    }

    function scheduleExpiry(session: turnkey.Session) {
      if (expiry_timeout) clearTimeout(expiry_timeout)
      expiry_timeout = undefined

      const delay = Math.max(session.expiry * 1000 - Date.now() - sessionSkewMs, 0)
      expiry_timeout = setTimeout(() => clear(), delay)
    }

    async function getValidSession() {
      const turnkeyClient = await getTurnkeyClient()
      const session = await turnkeyClient.getSession()

      if (!session || session.expiry * 1000 - sessionSkewMs <= Date.now()) {
        clear()
        return undefined
      }

      scheduleExpiry(session)
      return session
    }

    async function restore() {
      await Store.waitForHydration(store)
      if (walletAccounts.length > 0) return
      if (restore_promise) return await restore_promise

      restore_promise = (async () => {
        const state = store.getState()
        const persisted = state.accounts
        if (persisted.length === 0) return

        const session = await getValidSession()
        if (!session) return

        const restored = await fetchWalletAccounts()
        walletAccounts = persisted
          .map((account) =>
            restored.find((walletAccount) =>
              isAddressEqual(core_Address.from(walletAccount.address), account.address),
            ),
          )
          .filter((account): account is turnkey.WalletAccount => !!account)

        if (walletAccounts.length === 0) return

        store.setState({
          accounts: walletAccounts.map((account) => toStoreAccount(account)),
          activeAccount: Math.min(state.activeAccount, walletAccounts.length - 1),
        })
      })()

      try {
        await restore_promise
      } finally {
        restore_promise = undefined
      }
    }

    async function requireSession() {
      const session = await getValidSession()
      if (!session) throw new ox_Provider.DisconnectedError({ message: 'Turnkey session expired.' })
    }

    async function accountForSigning(address: Address | undefined) {
      await restore()
      await requireSession()

      const address_ = address ?? store.getState().accounts[store.getState().activeAccount]?.address
      if (!address_) throw new ox_Provider.DisconnectedError({ message: 'No accounts connected.' })

      const account = walletAccounts.find((account) =>
        isAddressEqual(core_Address.from(account.address), address_),
      )
      if (account) return account

      if (walletAccounts.length === 0)
        throw new ox_Provider.DisconnectedError({
          message: 'No Turnkey account connected.',
        })

      throw new ox_Provider.UnauthorizedError({ message: `Account "${address_}" not found.` })
    }

    function signatureToHex(value: turnkey.SignatureResponse): Hex.Hex {
      const v = value.v.startsWith('0x') ? (value.v as Hex.Hex) : Hex.fromNumber(Number(value.v))

      return Hex.concat(value.r as Hex.Hex, value.s as Hex.Hex, Hex.padLeft(v, 1))
    }

    async function signPayload(parameters: {
      payload: Hex.Hex
      turnkeyClient: turnkey.Client
      walletAccount: turnkey.WalletAccount
    }) {
      const { payload, turnkeyClient, walletAccount } = parameters
      const result = await turnkeyClient.httpClient
        .signRawPayload({
          encoding: 'PAYLOAD_ENCODING_HEXADECIMAL',
          hashFunction: 'HASH_FUNCTION_NO_OP',
          payload,
          signWith: walletAccount.address,
        })
        .catch((error) => {
          if (!isSessionError(error)) throw error
          clear()
          throw new ox_Provider.DisconnectedError({ message: 'Turnkey session expired.' })
        })

      return signatureToHex(result)
    }

    async function withAccessKey<result>(
      options: {
        address?: Address | undefined
        calls?: Adapter.signTransaction.Parameters['calls']
        chainId?: number | undefined
      },
      fn: (
        account: TempoAccount.Account,
        keyAuthorization?: KeyAuthorization.Signed,
      ) => Promise<result>,
    ): Promise<{ account: TempoAccount.Account; result: result } | undefined> {
      const account = (() => {
        try {
          return getAccount({ ...options, signable: true })
        } catch {
          return undefined
        }
      })()
      if (!account || account.source !== 'accessKey') return undefined

      const keyAuthorization = AccessKey.getPending(account, { store })
      try {
        const result = await fn(account, keyAuthorization ?? undefined)
        return { account, result }
      } catch (error) {
        AccessKey.invalidate(account, error, { store })
        return undefined
      }
    }

    async function signTransaction(parameters: Adapter.signTransaction.Parameters) {
      const account = toTempoAccount(await accountForSigning(parameters.from))
      const { feePayer, ...rest } = parameters
      const viemClient = getClient({
        chainId: parameters.chainId,
        feePayer: feePayer === true ? undefined : feePayer,
      })
      const prepared = await prepareTransactionRequest(viemClient, {
        account,
        ...rest,
        ...(feePayer ? { feePayer: true } : {}),
        type: 'tempo',
      } as never)
      return await account.signTransaction(prepared as never)
    }

    function isSessionError(error: unknown) {
      const code = getTurnkeyErrorCode(error)
      return !!code && turnkeySessionErrorCodes.has(code)
    }

    function getTurnkeyErrorCode(error: unknown): string | undefined {
      if (!isObject(error)) return undefined

      if (typeof error.code === 'string') return error.code

      if (Array.isArray(error.details)) {
        for (const detail of error.details) {
          if (!isObject(detail)) continue
          if (typeof detail.turnkeyErrorCode === 'string') return detail.turnkeyErrorCode
        }
      }

      return getTurnkeyErrorCode(error.cause)
    }

    function isObject(value: unknown): value is Record<string, unknown> {
      return typeof value === 'object' && value !== null
    }

    void restore()

    return {
      cleanup() {
        if (expiry_timeout) clearTimeout(expiry_timeout)
      },
      actions: {
        async createAccount(parameters) {
          const { authorizeAccessKey, personalSign } = parameters
          if (personalSign && parameters.digest)
            throw new ox_Provider.ProviderRpcError(
              -32602,
              '`digest` and `personalSign` cannot both be set on `wallet_connect`.',
            )

          const turnkeyClient = await getTurnkeyClient()
          const addresses = options.createAccount
            ? await options.createAccount({ client: turnkeyClient, parameters })
            : await options.loadAccounts({
                client: turnkeyClient,
                parameters: {
                  authorizeAccessKey,
                  digest: parameters.digest,
                  ...(personalSign ? { personalSign } : {}),
                },
              })
          await requireSession()
          walletAccounts = selectWalletAccounts(await fetchWalletAccounts(), addresses)
          restore_promise = undefined

          const digest = personalSign ? hashMessage(personalSign.message) : parameters.digest
          const account = walletAccounts[0]
          const keyAuthorization = authorizeAccessKey
            ? account
              ? await AccessKey.authorize({
                  account: toTempoAccount(account),
                  chainId: getClient().chain.id,
                  parameters: authorizeAccessKey,
                  store,
                })
              : undefined
            : undefined

          return {
            accounts: walletAccounts.map((account, index) =>
              toStoreAccount(account, index === 0 ? parameters.name : undefined),
            ),
            ...(personalSign ? { personalSign: { message: personalSign.message } } : {}),
            ...(keyAuthorization ? { keyAuthorization } : {}),
            signature:
              digest && account
                ? await signPayload({
                    payload: digest,
                    turnkeyClient,
                    walletAccount: account,
                  })
                : undefined,
          }
        },
        async loadAccounts(parameters) {
          const { authorizeAccessKey, personalSign } =
            parameters ?? ({} as Adapter.loadAccounts.Parameters)
          if (personalSign && parameters?.digest)
            throw new ox_Provider.ProviderRpcError(
              -32602,
              '`digest` and `personalSign` cannot both be set on `wallet_connect`.',
            )

          const turnkeyClient = await getTurnkeyClient()
          const addresses = await options.loadAccounts({ client: turnkeyClient, parameters })
          await requireSession()
          walletAccounts = selectWalletAccounts(await fetchWalletAccounts(), addresses)
          restore_promise = undefined

          const digest = personalSign ? hashMessage(personalSign.message) : parameters?.digest
          const account = walletAccounts[0]
          const keyAuthorization =
            authorizeAccessKey && account
              ? await AccessKey.authorize({
                  account: toTempoAccount(account),
                  chainId: getClient().chain.id,
                  parameters: authorizeAccessKey,
                  store,
                })
              : undefined

          return {
            accounts: walletAccounts.map((account) => toStoreAccount(account)),
            ...(personalSign ? { personalSign: { message: personalSign.message } } : {}),
            ...(keyAuthorization ? { keyAuthorization } : {}),
            signature:
              digest && account
                ? await signPayload({
                    payload: digest,
                    turnkeyClient,
                    walletAccount: account,
                  })
                : undefined,
          }
        },
        async authorizeAccessKey(parameters) {
          const account = await accountForSigning(undefined)
          const keyAuthorization = await AccessKey.authorize({
            account: toTempoAccount(account),
            chainId: getClient().chain.id,
            parameters,
            store,
          })
          return { keyAuthorization, rootAddress: core_Address.from(account.address) }
        },
        async signPersonalMessage(parameters) {
          const turnkeyClient = await getTurnkeyClient()
          const account = await accountForSigning(parameters.address)
          return await signPayload({
            payload: hashMessage({ raw: parameters.data }),
            turnkeyClient,
            walletAccount: account,
          })
        },
        async signTransaction(parameters) {
          const result = await withAccessKey(
            { address: parameters.from, calls: parameters.calls, chainId: parameters.chainId },
            async (account, keyAuthorization) => {
              const { feePayer, ...rest } = parameters
              const viemClient = getClient({
                chainId: parameters.chainId,
                feePayer: feePayer === true ? undefined : feePayer,
              })
              const prepared = await prepareTransactionRequest(viemClient, {
                account,
                ...rest,
                ...(feePayer ? { feePayer: true } : {}),
                keyAuthorization,
                type: 'tempo',
              } as never)
              return await account.signTransaction(prepared as never)
            },
          )
          if (result !== undefined) return result.result
          return await signTransaction(parameters)
        },
        async signTypedData(parameters) {
          const turnkeyClient = await getTurnkeyClient()
          const account = await accountForSigning(parameters.address)
          const typedData = JSON.parse(parameters.data) as {
            domain: Record<string, unknown>
            message: Record<string, unknown>
            primaryType: string
            types: Record<string, unknown>
          }
          return await signPayload({
            payload: hashTypedData(typedData as never),
            turnkeyClient,
            walletAccount: account,
          })
        },
        async sendTransaction(parameters) {
          const result = await withAccessKey(
            { address: parameters.from, calls: parameters.calls, chainId: parameters.chainId },
            async (account, keyAuthorization) => {
              const { feePayer, ...rest } = parameters
              const viemClient = getClient({
                chainId: parameters.chainId,
                feePayer: feePayer === true ? undefined : feePayer,
              })
              const prepared = await prepareTransactionRequest(viemClient, {
                account,
                ...rest,
                ...(feePayer ? { feePayer: true } : {}),
                keyAuthorization,
                type: 'tempo',
              } as never)
              const signed = await account.signTransaction(prepared as never)
              return await viemClient.request({
                method: 'eth_sendRawTransaction' as never,
                params: [signed],
              })
            },
          )
          if (result !== undefined) {
            AccessKey.removePending(result.account, { store })
            return result.result
          }
          const signed = await signTransaction(parameters)
          const viemClient = getClient({
            chainId: parameters.chainId,
            feePayer: parameters.feePayer === true ? undefined : parameters.feePayer,
          })
          return await viemClient.request({
            method: 'eth_sendRawTransaction' as never,
            params: [signed],
          })
        },
        async sendTransactionSync(parameters) {
          const result = await withAccessKey(
            { address: parameters.from, calls: parameters.calls, chainId: parameters.chainId },
            async (account, keyAuthorization) => {
              const { feePayer, ...rest } = parameters
              const viemClient = getClient({
                chainId: parameters.chainId,
                feePayer: feePayer === true ? undefined : feePayer,
              })
              const prepared = await prepareTransactionRequest(viemClient, {
                account,
                ...rest,
                ...(feePayer ? { feePayer: true } : {}),
                keyAuthorization,
                type: 'tempo',
              } as never)
              const signed = await account.signTransaction(prepared as never)
              return await viemClient.request({
                method: 'eth_sendRawTransactionSync' as never,
                params: [signed],
              })
            },
          )
          if (result !== undefined) {
            AccessKey.removePending(result.account, { store })
            return result.result
          }
          const signed = await signTransaction(parameters)
          const viemClient = getClient({
            chainId: parameters.chainId,
            feePayer: parameters.feePayer === true ? undefined : parameters.feePayer,
          })
          return await viemClient.request({
            method: 'eth_sendRawTransactionSync' as never,
            params: [signed],
          })
        },
        async disconnect() {
          await (await getTurnkeyClient()).logout()
          clear()
        },
      },
    }
  })
}

export declare namespace turnkey {
  /** Options for {@link turnkey}. */
  type Options<client extends Client = Client> = {
    /** Existing Turnkey client, such as `TurnkeyClient` from `@turnkey/core`. */
    client: client
    /**
     * Creates/registers a Turnkey wallet account. UI is allowed. Defaults to `loadAccounts`.
     * May return selected addresses; the first address is treated as active by default.
     */
    createAccount?:
      | ((parameters: {
          /** Initialized Turnkey client. */
          client: client
          /** Provider create-account parameters. */
          parameters: Adapter.createAccount.Parameters
        }) => Promise<AccountSelection>)
      | undefined
    /** Data URI of the provider icon. @default Black 1×1 SVG. */
    icon?: `data:image/${string}` | undefined
    /**
     * Loads/logs into existing Turnkey wallet accounts. UI is allowed. May return selected
     * addresses; the first address is treated as active by default.
     */
    loadAccounts: (parameters: {
      /** Initialized Turnkey client. */
      client: client
      /** Provider load-accounts parameters. */
      parameters?: Adapter.loadAccounts.Parameters | undefined
    }) => Promise<AccountSelection>
    /** Display name of the provider. @default "Turnkey" */
    name?: string | undefined
    /** Reverse DNS identifier. @default "com.turnkey" */
    rdns?: string | undefined
    /** Milliseconds before Turnkey session expiry to proactively disconnect. @default 10000 */
    sessionSkewMs?: number | undefined
  }

  /**
   * Optional selected addresses returned from a Turnkey login/sign-up callback.
   * When omitted, all fetched Turnkey Ethereum accounts are used. When provided,
   * fetched accounts are ordered to match this list, and the first address is active by default.
   */
  type AccountSelection = readonly Address[] | void

  /** Minimal structural Turnkey client surface used by the adapter. */
  type Client = {
    /** Fetches wallets visible to the current Turnkey session. */
    fetchWallets: () => Promise<readonly Wallet[]>
    /** Returns the current Turnkey session, if any. */
    getSession: () => Promise<Session | null | undefined>
    /** Low-level Turnkey HTTP client. */
    httpClient: {
      /** Signs a raw payload with Turnkey. */
      signRawPayload: (parameters: SignRawPayloadParameters) => Promise<SignatureResponse>
    }
    /** Initializes the client. Called once by the adapter. */
    init?: (() => Promise<void> | void) | undefined
    /** Clears the current Turnkey session. */
    logout: () => Promise<void> | void
  }

  /** Minimal Turnkey session shape used by the adapter. */
  type Session = {
    /** Session expiry in Unix seconds. */
    expiry: number
  }

  /** Minimal structural Turnkey wallet shape used by the adapter. */
  type Wallet = {
    /** Wallet accounts. */
    accounts: readonly WalletAccount[]
  }

  /** Minimal structural Turnkey wallet account fetched by the adapter. */
  type WalletAccount = {
    /** EVM address for the Turnkey wallet account. */
    address: string
    /** Turnkey Ethereum address format. */
    addressFormat?: 'ADDRESS_FORMAT_ETHEREUM' | undefined
    /** Raw compressed secp256k1 public key for the Turnkey wallet account. */
    publicKey: string
  }

  /** Signature parts returned by Turnkey raw-payload signing. */
  type SignatureResponse = {
    /** Signature r value. */
    r: string
    /** Signature s value. */
    s: string
    /** Signature recovery id/value. */
    v: string
  }

  /** Parameters for low-level Turnkey raw payload signing. */
  type SignRawPayloadParameters = {
    /** Payload encoding. */
    encoding: 'PAYLOAD_ENCODING_HEXADECIMAL'
    /** Hash function Turnkey should apply. */
    hashFunction: 'HASH_FUNCTION_NO_OP'
    /** Payload digest. */
    payload: Hex.Hex
    /** Turnkey signer identifier. */
    signWith: string
  }
}
