import * as Address from 'ox/Address'
import * as Hash from 'ox/Hash'
import type * as Hex from 'ox/Hex'
import * as Json from 'ox/Json'
import { MultisigConfig } from 'ox/tempo'
import { BaseError } from '../../errors/base.js'
import { nativeMultisigFactory } from '../Addresses.js'
import type * as Store from '../Store.js'

/** A valid 48-owner config is much smaller; 64 KiB bounds hostile store parsing work. */
const maxStoredValueLength = 65_536

/** Zero commitment used before an account is initialized. */
const zeroCommitment = `0x${'00'.repeat(32)}` as const

/** Reads a cached multisig config. */
export async function read(
  store: Store.Store,
  options: read.Options,
): Promise<MultisigConfig.Config | null> {
  const { address, commitment } = options
  const value = await store.getItem(key({ address, commitment }))
  if (value === null || value === undefined) return null
  const config = deserialize(value)
  assertKey({ address, commitment, config })
  return config
}

export declare namespace read {
  /** Parameters for {@link read}. */
  export type Options = {
    /** Multisig account address. */
    address: Address.Address
    /** Config commitment observed onchain. */
    commitment: Hex.Hex
  }
}

/** Writes a validated multisig config. */
export async function write(
  store: Store.Store,
  options: write.Options,
): Promise<void> {
  const { address, commitment } = options
  const config = MultisigConfig.from(options.config)
  assertKey({ address, commitment, config })
  const value = serialize(config)
  await store.setItem(key({ address, commitment }), value)
  // Initialization commits the version-zero config without changing its version.
  if (commitment.toLowerCase() === zeroCommitment)
    await store.setItem(
      key({ address, commitment: MultisigConfig.getCommitment(config) }),
      value,
    )
}

export declare namespace write {
  /** Parameters for {@link write}. */
  export type Options = {
    /** Multisig account address. */
    address: Address.Address
    /** Config commitment used for lookup. */
    commitment: Hex.Hex
    /** Complete config. */
    config: MultisigConfig.Config
  }
}

/** Verifies that a config matches its account-and-commitment key. */
function assertKey(options: {
  address: Address.Address
  commitment: Hex.Hex
  config: MultisigConfig.Config
}) {
  const { address, commitment, config } = options
  if (!Address.validate(address) || !Hash.validate(commitment))
    throw new InvalidStoreValueError()
  if (config.version === 0n) {
    if (
      !Address.isEqual(
        MultisigConfig.getAddress(config, { factory: nativeMultisigFactory }),
        address,
      )
    )
      throw new InvalidStoreValueError()
    if (commitment.toLowerCase() === zeroCommitment) return
  }
  if (
    MultisigConfig.getCommitment(config).toLowerCase() !==
    commitment.toLowerCase()
  )
    throw new InvalidStoreValueError()
}

/** Deserializes a multisig config from storage. */
function deserialize(value: string): MultisigConfig.Config {
  try {
    if (value.length > maxStoredValueLength) throw new InvalidStoreValueError()
    return MultisigConfig.fromRpc(Json.parse(value) as never)
  } catch (cause) {
    if (cause instanceof InvalidStoreValueError) throw cause
    throw new InvalidStoreValueError({ cause })
  }
}

/** Returns the store key for an account and config commitment. */
function key(options: { address: Address.Address; commitment: Hex.Hex }) {
  const { address, commitment } = options
  return `multisig:config:${address.toLowerCase()}:${commitment.toLowerCase()}`
}

/** Serializes a multisig config for storage. */
function serialize(config: MultisigConfig.Config): string {
  try {
    const value = Json.stringify(MultisigConfig.toRpc(config))
    if (value.length > maxStoredValueLength) throw new InvalidStoreValueError()
    return value
  } catch (cause) {
    if (cause instanceof InvalidStoreValueError) throw cause
    throw new InvalidStoreValueError({ cause })
  }
}

/** Thrown when a stored multisig config is malformed or mismatched. */
export class InvalidStoreValueError extends BaseError {
  /** Creates an invalid store value error. */
  constructor(options: InvalidStoreValueError.Options = {}) {
    super('Stored multisig config is malformed or mismatched.', {
      cause: options.cause as Error | undefined,
      name: 'Multisig.Config.InvalidStoreValueError',
    })
  }
}

export declare namespace InvalidStoreValueError {
  /** Error construction options. */
  export type Options = {
    /** Underlying error. */
    cause?: unknown | undefined
  }
}
