import { AbiFunction, Address, Hex, PublicKey, RpcResponse, WebCryptoP256 } from 'ox'
import { KeyAuthorization } from 'ox/tempo'
import { BaseError, type Client, type Transport } from 'viem'
import {
  Account as TempoAccount,
  Actions,
  KeyAuthorizationManager as TempoKeyAuthorizationManager,
} from 'viem/tempo'
import type { StoreApi } from 'zustand'

import type { OneOf } from '../internal/types.js'
import * as ExecutionError from './ExecutionError.js'
import * as Keystore from './Keystore.js'
import type * as Store from './Store.js'

const status = {
  /** No matching usable access key was found. */
  missing: 'missing',
  /** A matching key has a stored authorization that has not been observed on-chain yet. */
  pending: 'pending',
  /** A matching key exists on-chain and can be used. */
  published: 'published',
  /** A matching key exists but is past its expiry. */
  expired: 'expired',
} as const

const unavailableErrorNames = new Set(['KeyAlreadyRevoked', 'KeyNotFound'])

type Status = (typeof status)[keyof typeof status]

/** Access key entry stored alongside accounts. */
export type AccessKey = {
  /** Access key address. */
  address: Address.Address
  /** Owner of the access key. */
  access: Address.Address
  /** Chain ID this access key authorization is scoped to. */
  chainId: number
  /** Unix timestamp when the access key expires. */
  expiry?: number | undefined
  /** Signed key authorization managed by viem until the key is observed on-chain. */
  keyAuthorization?: KeyAuthorization.Signed | undefined
  /** Key type. */
  keyType: 'secp256k1' | 'p256' | 'webAuthn' | 'webCrypto'
  /** TIP-20 spending limits for the access key. */
  limits?: { token: Address.Address; limit: bigint; period?: number | undefined }[] | undefined
  /** Call scopes restricting which contracts/selectors this key can call. */
  scopes?:
    | {
        address: Address.Address
        selector?: Hex.Hex | string | undefined
        recipients?: readonly Address.Address[] | undefined
      }[]
    | undefined
} & OneOf<
  | {}
  | {
      /** The exported private key backing the access key. */
      privateKey: Hex.Hex
    }
  | {
      /** The WebCrypto key pair backing the access key. */
      keyPair: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>>
    }
  | {
      /** Opaque keystore handle backing the access key. Persisted verbatim; schema owned by the keystore that wrote it. */
      handle: Keystore.Handle
      /** Public key backing the access key. */
      publicKey: Hex.Hex
    }
>

/** Calls used to match access key scopes. */
export type Call = {
  /** Contract address being called. */
  to?: Address.Address | undefined
  /** Calldata being sent. */
  data?: Hex.Hex | undefined
}

/** Access key status query. */
type StatusQuery = {
  /** Root account address. */
  account: Address.Address
  /** Specific access key address to match. */
  accessKey?: Address.Address | undefined
  /** Calls to match against access key scopes. */
  calls?: readonly Call[] | undefined
  /** Chain ID the access key must be authorized on. */
  chainId: number
  /** Client used to verify publication state on-chain. */
  client: Client<Transport>
  /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
  now?: number | undefined
  /** Access-key manager options. */
  store: ManagerOptions
}

/** Access key selection query. */
type SelectQuery = {
  /** Root account address. */
  account: Address.Address
  /** Calls to match against access key scopes. */
  calls?: readonly Call[] | undefined
  /** Chain ID the access key must be authorized on. */
  chainId: number
  /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
  now?: number | undefined
  /** Access-key manager options. */
  store: ManagerOptions
}

/** Access key authorization reuse policy. */
export type ReusePolicy = {
  /** Minimum Unix timestamp a reusable key must be valid through. */
  minExpiry?: number | undefined
  /** Minimum spending limits a reusable key must satisfy. */
  minLimits?: readonly KeyAuthorization.TokenLimit[] | undefined
}

/** Access key authorization parameters plus SDK-only reuse policy. */
export type ReusableAuthorization = Omit<prepareAuthorization.Options, 'chainId' | 'keystores'> & {
  /** Chain ID the key authorization is scoped to. */
  chainId?: bigint | number | undefined
  /** SDK-only reuse policy. Not sent over RPC. */
  reuse?: ReusePolicy | undefined
}

type ReusableQuery = {
  /** Root account address. */
  account: Address.Address
  /** Calls the access key must be able to sign. */
  calls?: readonly Call[] | undefined
  /** Chain ID the access key must be authorized on. */
  chainId: number
  /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
  now?: number | undefined
  /** Access key authorization parameters with optional reuse policy. */
  parameters: ReusableAuthorization
  /** Access-key manager options. */
  store: ManagerOptions
}

type CallsQuery = {
  /** Calls the authorization must be able to sign. */
  calls?: readonly Call[] | undefined
  /** Access key authorization parameters. */
  parameters: Pick<ReusableAuthorization, 'scopes'>
}

type ListQuery = {
  /** Root account address. */
  account: Address.Address
  /** Specific access key address to match. */
  accessKey?: Address.Address | undefined
  /** Chain ID the access key is scoped to. */
  chainId: number
  /** Access-key manager dependencies. */
  store: ManagerOptions
}

type MatchQuery = {
  /** Calls the access key must be able to sign. */
  calls?: readonly Call[] | undefined
}

type ManagerOptions = {
  /** Keystores backing access-key records that carry an opaque `handle`. */
  keystores: Keystore.Keystores
  /** Zustand store containing access-key metadata. */
  state: Pick<StoreApi<Store.State>, 'getState' | 'setState'>
}

/** Access-key identity. */
type Key = {
  /** Root account address. */
  account: Address.Address
  /** Access key address. */
  accessKey: Address.Address
  /** Chain ID the access key is scoped to. */
  chainId: number
}

/** Store-bound access-key operations. */
type Manager = {
  /** Adds a signed access-key authorization. */
  add: (options: Omit<add.Options, 'store'>) => add.ReturnType
  /** Prepares, signs, and saves an access key authorization. */
  authorize: (options: Omit<authorize.Options, 'store'>) => Promise<authorize.ReturnType>
  /** Clears all access-key records. */
  clear: () => void
  /** Prepares an unsigned key authorization, creating key material via the keystore when needed. */
  prepareAuthorization: (
    options: Omit<prepareAuthorization.Options, 'keystore'>,
  ) => Promise<prepareAuthorization.ReturnType>
  /** Returns publication status for a stored or on-chain access key. */
  getStatus: (options: Omit<StatusQuery, 'store'>) => Promise<Status>
  /** Returns a locally-signable access key account by exact address. */
  get: (options: Omit<get.Options, 'store'>) => Promise<get.ReturnType>
  /** Returns access-key metadata matching a query. */
  list: (options: Omit<ListQuery, 'store'>) => readonly AccessKey[]
  /** Removes an access-key record. */
  remove: (options: Omit<remove.Options, 'store'>) => void
  /** Selects a locally-signable access key account for an intent. */
  select: (
    options: Omit<SelectQuery, 'store'>,
  ) => Promise<TempoAccount.AccessKeyAccount | undefined>
  /** Updates stored authorization metadata for an existing access key. */
  updateAuthorization: (options: Omit<updateAuthorization.Options, 'store'>) => void
}

/** Creates store-bound access-key operations. */
export function createManager(options: createManager.Options): Manager {
  return {
    add: (parameters) => add({ ...parameters, store: options }),
    authorize: (parameters) => authorize({ ...parameters, store: options }),
    clear: () => clear({ store: options }),
    get: (parameters) => get({ ...parameters, store: options }),
    prepareAuthorization: (parameters) =>
      prepareAuthorization({ ...parameters, keystores: options.keystores }),
    getStatus: (parameters) => getStatus({ ...parameters, store: options }),
    list: (parameters) => list({ ...parameters, store: options }),
    remove: (parameters) => remove({ ...parameters, store: options }),
    select: (parameters) => select({ ...parameters, store: options }),
    updateAuthorization: (parameters) => updateAuthorization({ ...parameters, store: options }),
  }
}

export declare namespace createManager {
  /** Options for {@link createManager}. */
  type Options = ManagerOptions
}

/** Prepares an unsigned key authorization and local key material when needed. */
export async function prepareAuthorization(
  options: prepareAuthorization.Options,
): Promise<prepareAuthorization.ReturnType> {
  const {
    address,
    chainId,
    expiry,
    keystores,
    keyType,
    limits,
    privateKey,
    publicKey,
    scopes,
    witness,
  } = options

  if (privateKey) {
    const type = keyType ?? 'secp256k1'
    const accessKey = (() => {
      switch (type) {
        case 'secp256k1':
          return TempoAccount.fromSecp256k1(privateKey)
        case 'p256':
          return TempoAccount.fromP256(privateKey)
        case 'webAuthn':
          throw new RpcResponse.InvalidParamsError({
            message: '`privateKey` cannot be used with `keyType: "webAuthn"`.',
          })
      }
    })()
    const keyAuthorization = KeyAuthorization.from({
      address: accessKey.address,
      chainId: BigInt(chainId),
      expiry,
      limits,
      scopes,
      type,
      ...(witness ? { witness } : {}),
    })
    return { keyAuthorization, privateKey }
  }

  if (address || publicKey) {
    const keyAuthorization = KeyAuthorization.from({
      address: address ?? Address.fromPublicKey(PublicKey.from(publicKey!)),
      chainId: BigInt(chainId),
      expiry,
      limits,
      scopes,
      type: keyType ?? 'secp256k1',
      ...(witness ? { witness } : {}),
    })
    return { keyAuthorization }
  }
  // p256 is the default key type everywhere (WebCrypto-backed where
  // available, safest); secp256k1 is opt-in via an explicit `keyType`.
  const keystores_ = keystores ?? Keystore.defaults
  const type = keyType ?? 'p256'
  const keystore = type === 'webAuthn' ? undefined : keystores_[type]
  if (!keystore)
    throw new RpcResponse.InvalidParamsError({
      message: `\`keyType: "${type}"\` requires externally generated key material; provide \`publicKey\` or \`address\`.`,
    })
  const key = await keystore.createKey()
  const keyAuthorization = KeyAuthorization.from({
    address: Address.fromPublicKey(PublicKey.fromHex(key.publicKey)),
    chainId: BigInt(chainId),
    expiry,
    limits,
    scopes,
    type,
    ...(witness ? { witness } : {}),
  })
  return { key: { handle: key.handle, publicKey: key.publicKey }, keyAuthorization }
}

export declare namespace prepareAuthorization {
  /** Options for {@link prepareAuthorization}. */
  type Options = {
    /** External access key address. Alternative to `publicKey`. */
    address?: Address.Address | undefined
    /** Chain ID the key authorization is scoped to. */
    chainId: bigint | number
    /** Unix timestamp when the key expires. */
    expiry: number
    /**
     * Keystores used to create key material when none is provided.
     * @default Keystore.defaults
     */
    keystores?: Keystore.Keystores | undefined
    /** External key type. Defaults to `secp256k1` for external keys. */
    keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
    /** TIP-20 spending limits for this key. */
    limits?: readonly KeyAuthorization.TokenLimit[] | undefined
    /** Exported private key backing the access key. */
    privateKey?: Hex.Hex | undefined
    /** External public key to derive the access key address from. */
    publicKey?: Hex.Hex | undefined
    /** Call scopes restricting which contracts/selectors this key can call. */
    scopes?: readonly KeyAuthorization.Scope[] | undefined
    /**
     * TIP-1053 witness (32 bytes) to bind into the key authorization. Set to
     * `hashMessage(message)` to fuse a Sign-In-with-Tempo proof into the
     * access-key authorization so both are covered by a single signature.
     */
    witness?: Hex.Hex | undefined
  }

  /** Prepared unsigned key authorization and optional local key material. */
  type ReturnType = {
    /** Keystore-created key material reference. */
    key?: { handle: Keystore.Handle; publicKey: Hex.Hex } | undefined
    /** Unsigned key authorization to sign with the root account. */
    keyAuthorization: KeyAuthorization.KeyAuthorization<false>
    /** Exported private key backing an external access key. */
    privateKey?: Hex.Hex | undefined
  }
}

/** Prepares, signs, and saves an access key authorization. */
export async function authorize(options: authorize.Options): Promise<authorize.ReturnType> {
  const { account, chainId, parameters } = options
  const { store } = options
  const prepared = await prepareAuthorization({
    ...parameters,
    chainId: parameters.chainId ?? chainId,
    keystores: store.keystores,
  })
  const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
  const signature = await account.sign({ hash: digest })
  const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
    signature,
  })

  add({
    account: account.address,
    authorization: keyAuthorization,
    ...(prepared.key ? { handle: prepared.key.handle, publicKey: prepared.key.publicKey } : {}),
    ...(prepared.privateKey ? { privateKey: prepared.privateKey } : {}),
    store,
  })

  return KeyAuthorization.toRpc(keyAuthorization)
}

export declare namespace authorize {
  /** Options for {@link authorize}. */
  type Options = {
    /** Root account that owns this access key and signs its authorization. */
    account: Pick<TempoAccount.Account, 'address' | 'sign'>
    /** Default chain ID for the authorization when `parameters.chainId` is not set. */
    chainId: bigint | number
    /** Access key authorization parameters. */
    parameters: Omit<prepareAuthorization.Options, 'chainId' | 'keystores'> & {
      /** Chain ID the key authorization is scoped to. */
      chainId?: bigint | number | undefined
    }
    /** Reactive state store. */
    store: ManagerOptions
  }

  /** Signed key authorization in RPC form. */
  type ReturnType = KeyAuthorization.Rpc
}

/** Returns whether a local access key satisfies reusable authorization parameters. */
export async function hasReusableAuthorization(options: ReusableQuery): Promise<boolean> {
  const { account, calls, chainId, parameters, store } = options
  const now = options.now ?? Date.now() / 1000
  const records = list({ account, chainId, store })

  for (const record of records) {
    if (isExpired(record.expiry, now)) continue
    if (!authorizationMatches(record, parameters)) continue
    if (calls && !recordScopesMatch(record, { calls })) continue
    if (!(await hydrate(record, store))) continue
    return true
  }
  return false
}

/** Returns whether an authorization request could sign the provided calls. */
export function canAuthorizeCalls(options: CallsQuery): boolean {
  return scopesMatch(options.parameters.scopes, { calls: options.calls })
}

/** Returns publication status for a stored or on-chain access key. */
export async function getStatus(options: StatusQuery): Promise<Status> {
  const { accessKey, account, calls, chainId, client } = options
  const { store } = options
  const now = options.now ?? Date.now() / 1000
  const local = list({ account, accessKey, chainId, store }).find((key) =>
    recordScopesMatch(key, { calls }),
  )

  if (local) {
    if (isExpired(local.expiry, now)) return status.expired
    if (local.keyAuthorization) {
      const publicationStatus = await getPublishedStatus(client, {
        accessKey: local.address,
        account,
        now,
      })
      if (publicationStatus === status.published)
        clearAuthorization({
          accessKey: local.address,
          account,
          chainId,
          store,
        })
      return publicationStatus === status.published ? status.published : status.pending
    }
    return await getPublishedStatus(client, { accessKey: local.address, account, now })
  }

  if (accessKey) return await getPublishedStatus(client, { accessKey, account, now })
  return status.missing
}

/** Selects a locally-signable access key account for an intent. */
export async function select(
  options: SelectQuery,
): Promise<TempoAccount.AccessKeyAccount | undefined> {
  const { account, calls, chainId, store } = options
  const now = options.now ?? Date.now() / 1000
  const records = list({ account, chainId, store })

  for (const record of records) {
    if (!recordScopesMatch(record, { calls })) continue
    if (isExpired(record.expiry, now)) {
      await remove({
        accessKey: record.address,
        account: record.access,
        chainId: record.chainId,
        store,
      })
      continue
    }

    const account_accessKey = await hydrate(record, store)
    if (!account_accessKey) continue

    return account_accessKey
  }
}

/** Returns a locally-signable access key account by exact address. */
export async function get(options: get.Options): Promise<get.ReturnType> {
  const { accessKey, account, calls, chainId } = options
  const { store } = options
  const now = options.now ?? Date.now() / 1000
  const record = list({ account, accessKey, chainId, store })[0]
  if (!record) return undefined
  if ('calls' in options && !recordScopesMatch(record, { calls })) return undefined
  if (isExpired(record.expiry, now)) {
    await remove({
      accessKey: record.address,
      account: record.access,
      chainId: record.chainId,
      store,
    })
    return undefined
  }
  return await hydrate(record, store)
}

export declare namespace get {
  type Options = MatchQuery & {
    /** Root account address. */
    account: Address.Address
    /** Specific access key address to match. */
    accessKey: Address.Address
    /** Chain ID the access key must be authorized on. */
    chainId: number
    /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
    now?: number | undefined
    /** Reactive state store. */
    store: ManagerOptions
  }

  type ReturnType = TempoAccount.AccessKeyAccount | undefined
}

function createKeyAuthorizationManager(store: ManagerOptions) {
  return TempoKeyAuthorizationManager.from({
    source: {
      get(key) {
        return list({
          account: key.address,
          accessKey: key.accessKey,
          chainId: key.chainId,
          store,
        })[0]?.keyAuthorization
      },
      remove(key) {
        clearAuthorization({
          account: key.address,
          accessKey: key.accessKey,
          chainId: key.chainId,
          store,
        })
      },
      set(key, keyAuthorization) {
        updateAuthorization({
          account: key.address,
          accessKey: key.accessKey,
          authorization: keyAuthorization,
          chainId: key.chainId,
          store,
        })
      },
    },
  })
}

/** Adds a signed access key authorization. */
export function add(options: add.Options): add.ReturnType {
  const { account, authorization, handle, keyPair, privateKey, publicKey } = options
  const { store } = options
  const base = {
    address: authorization.address,
    access: account,
    chainId: Number(authorization.chainId),
    expiry: authorization.expiry ?? undefined,
    keyAuthorization: authorization,
    keyType: authorization.type,
    limits: authorization.limits as AccessKey['limits'],
    scopes: authorization.scopes as AccessKey['scopes'],
  }
  const material = privateKey
    ? { privateKey }
    : keyPair
      ? { keyPair }
      : typeof handle !== 'undefined' && publicKey
        ? { handle, publicKey }
        : {}
  const record = { ...base, ...material } as AccessKey
  store.state.setState((state) => ({
    accessKeys: [
      record,
      ...state.accessKeys.filter(
        (entry) =>
          !matches(entry, {
            account: record.access,
            accessKey: record.address,
            chainId: record.chainId,
          }),
      ),
    ],
  }))
  return record
}

export declare namespace add {
  /** Options for {@link add}. */
  type Options = {
    /** Root account address that owns this access key. */
    account: Address.Address
    /** Signed key authorization for the access key. */
    authorization: KeyAuthorization.Signed
    /** Opaque keystore handle backing the access key. Requires `publicKey`. */
    handle?: Keystore.Handle | undefined
    /** The exported private key backing the access key. */
    privateKey?: Hex.Hex | undefined
    /** The WebCrypto key pair backing the access key. */
    keyPair?: Awaited<globalThis.ReturnType<typeof WebCryptoP256.createKeyPair>> | undefined
    /** Public key backing a keystore-managed access key. */
    publicKey?: Hex.Hex | undefined
    /** Reactive state store. */
    store: ManagerOptions
  }

  /** Stored access key record. */
  type ReturnType = AccessKey
}

function clearAuthorization(options: Key & { store: ManagerOptions }): void {
  const { store, ...key } = options
  patch({
    ...key,
    patch: { keyAuthorization: undefined },
    store,
  })
}

function updateAuthorization(options: updateAuthorization.Options): void {
  const { authorization, store, ...key } = options
  patch({
    ...key,
    patch: {
      expiry: authorization.expiry ?? undefined,
      keyAuthorization: authorization,
      limits: authorization.limits as AccessKey['limits'],
      scopes: authorization.scopes as AccessKey['scopes'],
    },
    store,
  })
}

declare namespace updateAuthorization {
  type Options = Key & {
    /** Signed key authorization for the access key. */
    authorization: KeyAuthorization.Signed
    /** Reactive state store. */
    store: ManagerOptions
  }
}

/** Removes an access key record. */
export function remove(options: remove.Options): void {
  const { store, ...key } = options
  store.state.setState((state) => ({
    accessKeys: state.accessKeys.filter((record) => !matches(record, key)),
  }))
}

export declare namespace remove {
  /** Options for {@link remove}. */
  type Options = Key & {
    /** Reactive state store. */
    store: ManagerOptions
  }
}

/** Clears all access-key records. */
function clear(options: { store: ManagerOptions }): void {
  const { store } = options
  store.state.setState({ accessKeys: [] })
}

/** Returns whether an error means an access key is already unavailable on-chain. */
export function isUnavailableError(error: unknown): boolean {
  if (error instanceof BaseError) {
    const found = error.walk((e) => {
      const errorName = (e as { data?: { errorName?: string } }).data?.errorName
      return !!errorName && unavailableErrorNames.has(errorName)
    })
    if (found) return true
  }

  if (!(error instanceof Error)) return false
  return unavailableErrorNames.has(ExecutionError.parse(error).errorName)
}

function recordScopesMatch(
  key: AccessKey,
  options: {
    calls?: readonly Call[] | undefined
  },
): boolean {
  return scopesMatch(key.scopes, options)
}

function scopesMatch(
  scopes: readonly NonNullable<AccessKey['scopes']>[number][] | undefined,
  options: {
    calls?: readonly Call[] | undefined
  },
): boolean {
  if (typeof scopes === 'undefined') return true
  if (!Array.isArray(scopes)) return false
  if (!options.calls) return false
  return options.calls.every((call) => {
    if (!call.to) return false
    const callTo = call.to.toLowerCase()
    const callSelector = call.data?.slice(0, 10).toLowerCase()
    return scopes.some((scope) => {
      if (!isScope(scope)) return false
      if (scope.address.toLowerCase() !== callTo) return false
      const selector = scope.selector
      if (!selector) return scope.recipients ? scope.recipients.length === 0 : true
      const scopeSelector = normalizeSelector(selector)
      if (!scopeSelector || callSelector !== scopeSelector) return false
      if (!scope.recipients || scope.recipients.length === 0) return true
      if (!call.data || call.data.length < 74) return false
      const recipient = `0x${call.data.slice(34, 74)}` as Address.Address
      if (!Address.validate(recipient)) return false
      return scope.recipients.some((address) => address.toLowerCase() === recipient.toLowerCase())
    })
  })
}

function authorizationMatches(key: AccessKey, parameters: ReusableAuthorization): boolean {
  if (!scopesCover(key.scopes, parameters.scopes)) return false
  if (
    typeof parameters.reuse?.minExpiry === 'number' &&
    key.expiry &&
    key.expiry < parameters.reuse.minExpiry
  )
    return false
  if (!limitsCover(key.limits, parameters.reuse?.minLimits)) return false
  return true
}

function scopesCover(
  existing: AccessKey['scopes'],
  requested: ReusableAuthorization['scopes'],
): boolean {
  if (!requested) return true
  if (!existing) return true
  return requested.every((scope) => existing.some((candidate) => scopeCovers(candidate, scope)))
}

function scopeCovers(
  existing: NonNullable<AccessKey['scopes']>[number],
  requested: NonNullable<ReusableAuthorization['scopes']>[number],
): boolean {
  if (!isScope(existing) || !isScope(requested)) return false
  if (existing.address.toLowerCase() !== requested.address.toLowerCase()) return false
  if (!selectorCovers(existing.selector, requested.selector)) return false
  return recipientsCover(existing.recipients, requested.recipients)
}

function selectorCovers(existing: string | undefined, requested: string | undefined): boolean {
  if (!existing) return true
  if (!requested) return false
  const selector_existing = normalizeSelector(existing)
  const selector_requested = normalizeSelector(requested)
  return !!selector_existing && selector_existing === selector_requested
}

function normalizeSelector(value: string): string | undefined {
  try {
    return (
      value.startsWith('0x') && value.length === 10 ? value : AbiFunction.getSelector(value)
    ).toLowerCase()
  } catch {
    return undefined
  }
}

function recipientsCover(
  existing: readonly Address.Address[] | undefined,
  requested: readonly Address.Address[] | undefined,
): boolean {
  if (!existing || existing.length === 0) return true
  if (!requested || requested.length === 0) return false
  return requested.every((address) =>
    existing.some((candidate) => candidate.toLowerCase() === address.toLowerCase()),
  )
}

function limitsCover(
  existing: AccessKey['limits'],
  requested: readonly KeyAuthorization.TokenLimit[] | undefined,
): boolean {
  if (!requested) return true
  if (!existing) return true
  return requested.every((limit) =>
    existing.some(
      (candidate) =>
        candidate.token.toLowerCase() === limit.token.toLowerCase() &&
        Number(candidate.period ?? 0) === Number(limit.period ?? 0) &&
        BigInt(candidate.limit) >= BigInt(limit.limit),
    ),
  )
}

function isScope(scope: unknown): scope is NonNullable<AccessKey['scopes']>[number] {
  if (!scope || typeof scope !== 'object') return false
  const value = scope as {
    address?: unknown
    recipients?: unknown
    selector?: unknown
  }
  if (typeof value.address !== 'string' || !Address.validate(value.address)) return false
  if (typeof value.selector !== 'undefined' && typeof value.selector !== 'string') return false
  if (typeof value.recipients !== 'undefined') {
    if (!Array.isArray(value.recipients)) return false
    if (value.recipients.some((recipient) => typeof recipient !== 'string')) return false
    if (value.recipients.some((recipient) => !Address.validate(recipient))) return false
  }
  return true
}

/** Keystore accounts hydrated this launch, keyed by record identity. */
const keystoreAccounts = new WeakMap<object, Promise<TempoAccount.AccessKeyAccount>>()

async function hydrate(
  accessKey: AccessKey,
  store: ManagerOptions,
): Promise<TempoAccount.AccessKeyAccount | undefined> {
  const keyAuthorizationManager = createKeyAuthorizationManager(store)
  if ('keyPair' in accessKey && accessKey.keyPair)
    return TempoAccount.fromWebCryptoP256(accessKey.keyPair, {
      access: accessKey.access,
      keyAuthorizationManager,
    }) as TempoAccount.AccessKeyAccount
  if ('privateKey' in accessKey && accessKey.privateKey) {
    switch (accessKey.keyType) {
      case 'secp256k1':
        return TempoAccount.fromSecp256k1(accessKey.privateKey, {
          access: accessKey.access,
          keyAuthorizationManager,
        }) as TempoAccount.AccessKeyAccount
      case 'p256':
        return TempoAccount.fromP256(accessKey.privateKey, {
          access: accessKey.access,
          keyAuthorizationManager,
        }) as TempoAccount.AccessKeyAccount
    }
  }
  if ('handle' in accessKey && typeof accessKey.handle !== 'undefined' && accessKey.publicKey) {
    const keystore =
      accessKey.keyType === 'p256' || accessKey.keyType === 'secp256k1'
        ? store.keystores[accessKey.keyType]
        : undefined
    if (!keystore) return undefined
    let account = keystoreAccounts.get(accessKey)
    if (!account) {
      account = (async () =>
        await keystore.toAccount(
          {
            handle: accessKey.handle,
            keyType: accessKey.keyType,
            publicKey: accessKey.publicKey!,
          },
          { access: accessKey.access, keyAuthorizationManager },
        ))()
      keystoreAccounts.set(accessKey, account)
    }
    try {
      return await account
    } catch (error) {
      // The backend cannot materialize this key right now — treat the record
      // as unusable so callers fall back to re-authorization. Uncached so
      // transient failures (e.g. device locked) can retry; keystore-signaled
      // permanent loss (e.g. hardware key deleted) evicts the record.
      keystoreAccounts.delete(accessKey)
      if (Keystore.isKeyUnavailableError(error))
        remove({
          accessKey: accessKey.address,
          account: accessKey.access,
          chainId: accessKey.chainId,
          store,
        })
      return undefined
    }
  }
  return undefined
}

function isExpired(expiry: number | undefined, now: number): boolean {
  return typeof expiry === 'number' && expiry < now
}

async function getPublishedStatus(
  client: Client<Transport>,
  options: { accessKey: Address.Address; account: Address.Address; now: number },
): Promise<Status> {
  const { accessKey, account, now } = options
  try {
    const metadata = await Actions.accessKey.getMetadata(client, {
      account,
      accessKey,
    })
    if (metadata.address.toLowerCase() !== accessKey.toLowerCase()) return status.missing
    if (metadata.isRevoked) return status.missing
    if (metadata.expiry > 0n && metadata.expiry < BigInt(Math.floor(now))) return status.expired
    return status.published
  } catch (error) {
    if (isUnavailableError(error)) return status.missing
    throw error
  }
}

function list(options: ListQuery): readonly AccessKey[] {
  const { store, ...query } = options
  return store.state.getState().accessKeys.filter((key) => matches(key, query))
}

function patch(options: Key & { patch: Partial<AccessKey>; store: ManagerOptions }): void {
  const { patch, store, ...key } = options
  store.state.setState((state) => ({
    accessKeys: state.accessKeys.map((record) => {
      if (!matches(record, key)) return record
      const next = { ...record } as Record<string, unknown>
      for (const [name, value] of Object.entries(patch)) {
        if (typeof value === 'undefined') delete next[name]
        else next[name] = value
      }
      return next as AccessKey
    }),
  }))
}

function matches(
  record: AccessKey,
  options: {
    account: Address.Address
    accessKey?: Address.Address | undefined
    chainId: number
  },
): boolean {
  const { accessKey, account, chainId } = options
  if (record.access.toLowerCase() !== account.toLowerCase()) return false
  if (record.chainId !== chainId) return false
  if (accessKey && record.address.toLowerCase() !== accessKey.toLowerCase()) return false
  return true
}
