import * as AbiParameters from '../core/AbiParameters.js'
import * as Address from '../core/Address.js'
import type * as Errors from '../core/Errors.js'
import * as Hash from '../core/Hash.js'
import * as Hex from '../core/Hex.js'

const channelIdParameters = AbiParameters.from(
  'address, address, address, address, bytes32, address, bytes32, address, uint256',
)
const domainSeparatorParameters = AbiParameters.from(
  'bytes32, bytes32, bytes32, uint256, address',
)
const voucherHashParameters = AbiParameters.from('bytes32, bytes32, uint96')

const eip712DomainTypehash = Hash.keccak256(
  Hex.fromString(
    'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)',
  ),
)
const nameHash = Hash.keccak256(Hex.fromString('TIP20 Channel Reserve'))
const versionHash = Hash.keccak256(Hex.fromString('1'))
const zeroAddress =
  '0x0000000000000000000000000000000000000000' as const satisfies Address.Address

/**
 * TIP-20 channel reserve precompile address.
 */
export const address =
  '0x4d50500000000000000000000000000000000000' as const satisfies Address.Address

/**
 * Delay between payer `requestClose` and `withdraw`, in seconds.
 */
export const closeGracePeriod = 900n

/**
 * EIP-712 type hash for `Voucher(bytes32 channelId,uint96 cumulativeAmount)`.
 */
export const voucherTypehash = Hash.keccak256(
  Hex.fromString('Voucher(bytes32 channelId,uint96 cumulativeAmount)'),
)

/**
 * TIP-20 channel descriptor.
 */
export type Channel = {
  /** Optional signer for vouchers. Zero means `payer` signs. */
  authorizedSigner: Address.Address
  /** Transaction-derived hash assigned when the channel was opened. */
  expiringNonceHash: Hex.Hex
  /** Optional relayer allowed to submit `settle` for the payee. */
  operator: Address.Address
  /** Account that receives settled voucher payments. */
  payee: Address.Address
  /** Account that funded the channel and receives refunds. */
  payer: Address.Address
  /** User-supplied salt to distinguish otherwise identical channels. */
  salt: Hex.Hex
  /** TIP-20 token address held by the channel. */
  token: Address.Address
}

/**
 * Instantiates a TIP-20 channel reserve descriptor.
 *
 * Defaults `operator` and `authorizedSigner` to the zero address.
 *
 * @example
 * ```ts twoslash
 * import { Channel } from 'ox/tempo'
 *
 * const channel = Channel.from({
 *   expiringNonceHash:
 *     '0x0000000000000000000000000000000000000000000000000000000000000002',
 *   payee: '0x2222222222222222222222222222222222222222',
 *   payer: '0x1111111111111111111111111111111111111111',
 *   salt: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   token: '0x20c0000000000000000000000000000000000001'
 * })
 * ```
 *
 * @param value - The channel descriptor input.
 * @returns The normalized channel descriptor.
 */
export function from(value: from.Value): from.ReturnType {
  const {
    authorizedSigner = zeroAddress,
    expiringNonceHash,
    operator = zeroAddress,
    payee,
    payer,
    salt,
    token,
  } = value

  return {
    authorizedSigner: resolveAddress(authorizedSigner),
    expiringNonceHash,
    operator: resolveAddress(operator),
    payee: resolveAddress(payee),
    payer: resolveAddress(payer),
    salt,
    token: resolveAddress(token),
  }
}

export declare namespace from {
  type Value = {
    /** Optional signer for vouchers. Zero means `payer` signs. */
    authorizedSigner?: Address.Address | undefined
    /** Transaction-derived hash assigned when the channel was opened. */
    expiringNonceHash: Hex.Hex
    /** Optional relayer allowed to submit `settle` for the payee. */
    operator?: Address.Address | undefined
    /** Account that receives settled voucher payments. */
    payee: Address.Address
    /** Account that funded the channel and receives refunds. */
    payer: Address.Address
    /** User-supplied salt to distinguish otherwise identical channels. */
    salt: Hex.Hex
    /** TIP-20 token address held by the channel. */
    token: Address.Address
  }

  type ReturnType = Channel

  type ErrorType = Address.from.ErrorType | Errors.GlobalErrorType
}

/**
 * Computes the canonical TIP-20 channel id for a descriptor.
 *
 * Mirrors `computeChannelId` on the TIP-20 channel reserve precompile without
 * performing an RPC call.
 *
 * @example
 * ```ts twoslash
 * import { Channel } from 'ox/tempo'
 *
 * const channelId = Channel.computeId(
 *   {
 *     authorizedSigner:
 *       '0x0000000000000000000000000000000000000000',
 *     expiringNonceHash:
 *       '0x0000000000000000000000000000000000000000000000000000000000000000',
 *     operator: '0x0000000000000000000000000000000000000000',
 *     payee: '0x2222222222222222222222222222222222222222',
 *     payer: '0x1111111111111111111111111111111111111111',
 *     salt: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *     token: '0x20c0000000000000000000000000000000000001'
 *   },
 *   {
 *     chainId: 4217
 *   }
 * )
 * ```
 *
 * @param channel - Channel descriptor.
 * @param options - Options.
 * @returns The channel id.
 */
export function computeId(
  channel: computeId.Channel,
  options: computeId.Options,
): Hex.Hex {
  const channel_ = from(channel)
  return Hash.keccak256(
    AbiParameters.encode(channelIdParameters, [
      channel_.payer,
      channel_.payee,
      channel_.operator,
      channel_.token,
      channel_.salt,
      channel_.authorizedSigner,
      channel_.expiringNonceHash,
      address,
      BigInt(options.chainId),
    ]),
  )
}

export declare namespace computeId {
  type Channel = from.Value

  type Options = {
    /** Chain ID used by the channel reserve precompile. */
    chainId: number | bigint
  }

  type ErrorType =
    | AbiParameters.encode.ErrorType
    | from.ErrorType
    | Hash.keccak256.ErrorType
}

/**
 * Computes the EIP-712 domain separator for the TIP-20 channel reserve.
 *
 * Mirrors `domainSeparator` on the TIP-20 channel reserve precompile without
 * performing an RPC call.
 *
 * @example
 * ```ts twoslash
 * import { Channel } from 'ox/tempo'
 *
 * const separator = Channel.domainSeparator({ chainId: 4217 })
 * ```
 *
 * @param value - Chain id.
 * @returns The EIP-712 domain separator.
 */
export function domainSeparator(value: domainSeparator.Value): Hex.Hex {
  return Hash.keccak256(
    AbiParameters.encode(domainSeparatorParameters, [
      eip712DomainTypehash,
      nameHash,
      versionHash,
      BigInt(value.chainId),
      address,
    ]),
  )
}

export declare namespace domainSeparator {
  type Value = {
    /** Chain id used by the channel reserve precompile. */
    chainId: number | bigint
  }

  type ErrorType = AbiParameters.encode.ErrorType | Hash.keccak256.ErrorType
}

/**
 * Computes the EIP-712 sign payload for a TIP-20 channel voucher.
 *
 * Mirrors `getVoucherDigest` on the TIP-20 channel reserve precompile without
 * performing an RPC call.
 *
 * @example
 * ```ts twoslash
 * import { Channel } from 'ox/tempo'
 *
 * const payload = Channel.getVoucherSignPayload({
 *   chainId: 4217,
 *   channelId:
 *     '0x0000000000000000000000000000000000000000000000000000000000000000',
 *   cumulativeAmount: 1n
 * })
 * ```
 *
 * @param value - Voucher fields and chain id.
 * @returns The voucher sign payload.
 */
export function getVoucherSignPayload(
  value: getVoucherSignPayload.Value,
): Hex.Hex {
  const voucherHash = Hash.keccak256(
    AbiParameters.encode(voucherHashParameters, [
      voucherTypehash,
      value.channelId,
      value.cumulativeAmount,
    ]),
  )
  return Hash.keccak256(
    Hex.concat(
      '0x1901',
      domainSeparator({ chainId: value.chainId }),
      voucherHash,
    ),
  )
}

export declare namespace getVoucherSignPayload {
  type Value = {
    /** Chain id used by the channel reserve precompile. */
    chainId: number | bigint
    /** Channel id. */
    channelId: Hex.Hex
    /** Total voucher amount signed for the channel. */
    cumulativeAmount: bigint
  }

  type ErrorType =
    | AbiParameters.encode.ErrorType
    | domainSeparator.ErrorType
    | Hash.keccak256.ErrorType
}

function resolveAddress(address: Address.Address): Address.Address {
  return Address.from(address)
}
