import type * as Address from './Address.js'
import type * as Errors from './Errors.js'
import * as Hex from './Hex.js'
import type { Compute, OneOf } from './internal/types.js'
import * as Transaction from './Transaction.js'
import * as Withdrawal from './Withdrawal.js'

/** A Block as defined in the [Execution API specification](https://github.com/ethereum/execution-apis/blob/main/src/schemas/block.yaml). */
export type Block<
  includeTransactions extends boolean = false,
  blockTag extends Tag = 'latest',
  bigintType = bigint,
  numberType = number,
  transaction = Transaction.Transaction<
    blockTag extends 'pending' ? true : false,
    bigintType,
    numberType
  >,
> = Compute<{
  /** Base fee per gas */
  baseFeePerGas?: bigintType | undefined
  /** Total used blob gas by all transactions in this block */
  blobGasUsed?: bigintType | undefined
  /** Difficulty for this block */
  difficulty?: bigintType | undefined
  /** Excess blob gas */
  excessBlobGas?: bigintType | undefined
  /** "Extra data" field of this block */
  extraData?: Hex.Hex | undefined
  /** Maximum gas allowed in this block */
  gasLimit: bigintType
  /** Total used gas by all transactions in this block */
  gasUsed: bigintType
  /** Block hash or `null` if pending */
  hash: blockTag extends 'pending' ? null : Hex.Hex
  /** Logs bloom filter or `null` if pending */
  logsBloom: blockTag extends 'pending' ? null : Hex.Hex
  /** Address that received this block’s mining rewards */
  miner: Address.Address
  /** Unique identifier for the block. */
  mixHash: Hex.Hex
  /** Proof-of-work hash or `null` if pending */
  nonce: blockTag extends 'pending' ? null : Hex.Hex
  /** Block number or `null` if pending */
  number: blockTag extends 'pending' ? null : bigintType
  parentBeaconBlockRoot?: Hex.Hex | undefined
  /** Parent block hash */
  parentHash: Hex.Hex
  /** Root of the this block’s receipts trie */
  receiptsRoot: Hex.Hex
  sealFields?: readonly Hex.Hex[] | undefined
  /** SHA3 of the uncles data in this block */
  sha3Uncles: Hex.Hex
  /** Size of this block in bytes */
  size: bigintType
  /** Root of this block’s final state trie */
  stateRoot: Hex.Hex
  /** Unix timestamp of when this block was collated */
  timestamp: bigintType
  /** Total difficulty of the chain until this block */
  totalDifficulty?: bigintType | undefined
  /** List of transaction objects or hashes */
  transactions: includeTransactions extends true
    ? readonly transaction[]
    : readonly Hex.Hex[]
  /** Root of this block’s transaction trie */
  transactionsRoot: Hex.Hex
  /** List of uncle hashes */
  uncles: readonly Hex.Hex[]
  /** List of withdrawal objects */
  withdrawals?:
    | readonly Withdrawal.Withdrawal<bigintType, numberType>[]
    | undefined
  /** Root of the this block’s withdrawals trie */
  withdrawalsRoot?: Hex.Hex | undefined
}>

/** A Block hash. */
export type Hash = Hex.Hex

/** A Block identifier. */
export type Identifier<bigintType = bigint> = {
  /** Whether or not to throw an error if the block is not in the canonical chain as described below. Only allowed in conjunction with the blockHash tag. Defaults to false. */
  requireCanonical?: boolean | undefined
} & OneOf<
  | {
      /** The block in the canonical chain with this number */
      blockNumber: Number<bigintType>
    }
  | {
      /** The block uniquely identified by this hash. The `blockNumber` and `blockHash` properties are mutually exclusive; exactly one of them must be set. */
      blockHash: Hash
    }
>

/** A Block number. */
export type Number<bigintType = bigint> = bigintType

/** An RPC Block as defined in the [Execution API specification](https://github.com/ethereum/execution-apis/blob/main/src/schemas/block.yaml). */
export type Rpc<
  includeTransactions extends boolean = boolean,
  blockTag extends Tag = 'latest',
  transaction = Transaction.Rpc<blockTag extends 'pending' ? true : false>,
> = Block<includeTransactions, blockTag, Hex.Hex, Hex.Hex, transaction>

/**
 * A Block Tag as defined in the [Execution API specification](https://github.com/ethereum/execution-apis/blob/main/src/schemas/block.yaml).
 *
 * - `earliest`: The lowest numbered block the client has available;
 * - `finalized`: The most recent crypto-economically secure block, cannot be re-orged outside of manual intervention driven by community coordination;
 * - `safe`: The most recent block that is safe from re-orgs under honest majority and certain synchronicity assumptions;
 * - `latest`: The most recent block in the canonical chain observed by the client, this block may be re-orged out of the canonical chain even under healthy/normal conditions;
 * - `pending`: A sample next block built by the client on top of `latest` and containing the set of transactions usually taken from local mempool.
 */
export type Tag = 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'

/**
 * Converts a {@link ox#Block.Block} to an {@link ox#Block.Rpc}.
 *
 * @example
 * ```ts twoslash
 * // @noErrors
 * import { Block } from 'ox'
 *
 * const block = Block.toRpc({
 *   // ...
 *   hash: '0xebc3644804e4040c0a74c5a5bbbc6b46a71a5d4010fe0c92ebb2fdf4a43ea5dd',
 *   number: 19868020n,
 *   size: 520n
 *   timestamp: 1662222222n,
 *   // ...
 * })
 * // @log: {
 * // @log:   // ...
 * // @log:   hash: '0xebc3644804e4040c0a74c5a5bbbc6b46a71a5d4010fe0c92ebb2fdf4a43ea5dd',
 * // @log:   number: '0xec6fc6',
 * // @log:   size: '0x208',
 * // @log:   timestamp: '0x63198f6f',
 * // @log:   // ...
 * // @log: }
 * ```
 *
 * @param block - The Block to convert.
 * @returns An RPC Block.
 */
export function toRpc<
  includeTransactions extends boolean = false,
  blockTag extends Tag = 'latest',
>(
  block: Block<includeTransactions, blockTag>,
  _options: toRpc.Options<includeTransactions, blockTag> = {},
): Rpc<boolean, blockTag> {
  const transactions = block.transactions.map((transaction) => {
    if (typeof transaction === 'string') return transaction
    return Transaction.toRpc(transaction as any) as any
  })
  return {
    baseFeePerGas:
      typeof block.baseFeePerGas === 'bigint'
        ? Hex.fromNumber(block.baseFeePerGas)
        : undefined,
    blobGasUsed:
      typeof block.blobGasUsed === 'bigint'
        ? Hex.fromNumber(block.blobGasUsed)
        : undefined,
    excessBlobGas:
      typeof block.excessBlobGas === 'bigint'
        ? Hex.fromNumber(block.excessBlobGas)
        : undefined,
    extraData: block.extraData,
    difficulty:
      typeof block.difficulty === 'bigint'
        ? Hex.fromNumber(block.difficulty)
        : undefined,
    gasLimit: Hex.fromNumber(block.gasLimit),
    gasUsed: Hex.fromNumber(block.gasUsed),
    hash: block.hash,
    logsBloom: block.logsBloom,
    miner: block.miner,
    mixHash: block.mixHash,
    nonce: block.nonce,
    number: (typeof block.number === 'bigint'
      ? Hex.fromNumber(block.number)
      : null) as never,
    parentBeaconBlockRoot: block.parentBeaconBlockRoot,
    parentHash: block.parentHash,
    receiptsRoot: block.receiptsRoot,
    sealFields: block.sealFields,
    sha3Uncles: block.sha3Uncles,
    size: Hex.fromNumber(block.size),
    stateRoot: block.stateRoot,
    timestamp: Hex.fromNumber(block.timestamp),
    totalDifficulty:
      typeof block.totalDifficulty === 'bigint'
        ? Hex.fromNumber(block.totalDifficulty)
        : undefined,
    transactions,
    transactionsRoot: block.transactionsRoot,
    uncles: block.uncles,
    withdrawals: block.withdrawals?.map(Withdrawal.toRpc),
    withdrawalsRoot: block.withdrawalsRoot,
  }
}

export declare namespace toRpc {
  type Options<
    includeTransactions extends boolean = false,
    blockTag extends Tag = 'latest',
  > = {
    blockTag?: blockTag | Tag | undefined
    includeTransactions?: includeTransactions | boolean | undefined
  }

  type ErrorType = Errors.GlobalErrorType
}

/**
 * Converts a {@link ox#Block.Rpc} to an {@link ox#Block.Block}.
 *
 * @example
 * ```ts twoslash
 * // @noErrors
 * import { Block } from 'ox'
 *
 * const block = Block.fromRpc({
 *   // ...
 *   hash: '0xebc3644804e4040c0a74c5a5bbbc6b46a71a5d4010fe0c92ebb2fdf4a43ea5dd',
 *   number: '0xec6fc6',
 *   size: '0x208',
 *   timestamp: '0x63198f6f',
 *   // ...
 * })
 * // @log: {
 * // @log:   // ...
 * // @log:   hash: '0xebc3644804e4040c0a74c5a5bbbc6b46a71a5d4010fe0c92ebb2fdf4a43ea5dd',
 * // @log:   number: 19868020n,
 * // @log:   size: 520n,
 * // @log:   timestamp: 1662222222n,
 * // @log:   // ...
 * // @log: }
 * ```
 *
 * @example
 * ### End-to-end
 *
 * Below is an end-to-end example of using `Block.fromRpc` to fetch a block from the network and convert it to an {@link ox#Block.Block}.
 *
 * ```ts twoslash
 * import 'ox/window'
 * import { Block } from 'ox'
 *
 * const block = await window.ethereum!
 *   .request({
 *     method: 'eth_getBlockByNumber',
 *     params: ['latest', false],
 *   })
 *   .then(Block.fromRpc) // [!code hl]
 * // @log: {
 * // @log:   // ...
 * // @log:   hash: '0xebc3644804e4040c0a74c5a5bbbc6b46a71a5d4010fe0c92ebb2fdf4a43ea5dd',
 * // @log:   number: 19868020n,
 * // @log:   size: 520n,
 * // @log:   timestamp: 1662222222n,
 * // @log:   // ...
 * // @log: }
 * ```
 *
 * :::note
 *
 * For simplicity, the above example uses `window.ethereum.request`, but you can use any
 * type of JSON-RPC interface.
 *
 * :::
 *
 * @param block - The RPC block to convert.
 * @returns An instantiated {@link ox#Block.Block}.
 */
export function fromRpc<
  const block extends Rpc | null,
  includeTransactions extends boolean = false,
  blockTag extends Tag = 'latest',
>(
  block: block | Rpc | null,
  _options: fromRpc.Options<includeTransactions, blockTag> = {},
): block extends Rpc ? Block<includeTransactions, blockTag> : null {
  if (!block) return null as never

  const transactions = block.transactions.map((transaction) => {
    if (typeof transaction === 'string') return transaction
    return Transaction.fromRpc(transaction) as any
  })
  return {
    ...block,
    baseFeePerGas: block.baseFeePerGas
      ? BigInt(block.baseFeePerGas)
      : undefined,
    blobGasUsed: block.blobGasUsed ? BigInt(block.blobGasUsed) : undefined,
    difficulty: block.difficulty ? BigInt(block.difficulty) : undefined,
    excessBlobGas: block.excessBlobGas
      ? BigInt(block.excessBlobGas)
      : undefined,
    gasLimit: BigInt(block.gasLimit ?? 0n),
    gasUsed: BigInt(block.gasUsed ?? 0n),
    number: block.number ? BigInt(block.number) : null,
    size: BigInt(block.size ?? 0n),
    stateRoot: block.stateRoot,
    timestamp: BigInt(block.timestamp ?? 0n),
    totalDifficulty: BigInt(block.totalDifficulty ?? 0n),
    transactions,
    withdrawals: block.withdrawals?.map(Withdrawal.fromRpc),
  } as Block as never
}

export declare namespace fromRpc {
  type Options<
    includeTransactions extends boolean = false,
    blockTag extends Tag = 'latest',
  > = {
    blockTag?: blockTag | Tag | undefined
    includeTransactions?: includeTransactions | boolean | undefined
  }

  type ErrorType = Errors.GlobalErrorType
}
